Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
761d87d
feat!: Spotify Web API breaking changes for 2026-02-11 (#448)
Copilot Feb 11, 2026
fbec5b9
chore(track): correct javadoc comment
dargmuesli Feb 11, 2026
786623b
ci: update signing key
dargmuesli Feb 11, 2026
d0dcfaf
10.0.0-RC1
dargmuesli Feb 11, 2026
a93ad50
feat(user): add create playlist endpoint
dargmuesli Mar 9, 2026
e3f71dc
ci(agents): add instructions
dargmuesli Mar 9, 2026
8f77f1d
ci(jitpack): use java 17
dargmuesli Mar 9, 2026
91bf5aa
Merge pull request #454 from spotify-web-api-java/copilot/add-create-…
dargmuesli Mar 9, 2026
a069569
10.0.0-RC2
dargmuesli Mar 9, 2026
557f20d
add common interfaces
fm-sys Apr 13, 2026
fcf3d08
apply copilot review suggestion
fm-sys Apr 13, 2026
01f413e
add getItems to IPlaylist to allow querying number of total tracks
fm-sys Apr 13, 2026
2b41d3e
refactor: sort imports
dargmuesli Apr 29, 2026
57276dd
fix(playlist): allow to extend the has-total interface
dargmuesli Apr 29, 2026
7ef43d2
Merge pull request #457 from fm-sys/common-interfaces
dargmuesli Apr 29, 2026
2841d7a
10.0.0-RC3
dargmuesli Apr 29, 2026
e3c0233
feat(api)!: sync specification (#460)
dargmuesli Aug 31, 2026
559da33
chore(spec): vendor spotify openapi schema for diffing (#469)
dargmuesli Aug 31, 2026
bcca48b
fix(javadoc): qualify cross-package links and document missing members
dargmuesli Aug 31, 2026
2eae502
refactor(playlists): deprecate legacy builders and reorder deprecatio…
dargmuesli Aug 31, 2026
372d951
docs(examples): rename examples to match their request classes
dargmuesli Aug 31, 2026
441e301
docs(examples): add examples for the 26 uncovered endpoints
dargmuesli Aug 31, 2026
33109e9
docs(readme): rebuild the example index from the code
dargmuesli Aug 31, 2026
e0baf0d
ci: verify examples, readme and javadoc on every build
dargmuesli Aug 31, 2026
59fec8b
Merge pull request #470 from spotify-web-api-java/fix/docs/align-java…
dargmuesli Aug 31, 2026
d280088
Merge branch 'main' into beta
dargmuesli Aug 31, 2026
d070e54
10.0.0-RC4
dargmuesli Aug 31, 2026
8dc27f7
docs: add a v9 to v10 migration guide (#471)
dargmuesli Sep 1, 2026
de189af
fix(models)!: deserialize model objects reflectively with gson (#473)
dargmuesli Sep 1, 2026
30ea468
refactor(renovate): add file extension
dargmuesli Sep 1, 2026
ce5f922
docs: correct the 9.4.0 deprecation count in the migration guide
dargmuesli Sep 1, 2026
7f65347
10.0.0-RC5
dargmuesli Sep 1, 2026
b22d303
fix(build): keep the automatic module name in the released jar
dargmuesli Sep 1, 2026
f46781a
10.0.0-RC6
dargmuesli Sep 1, 2026
384e973
feat(build)!: ship an explicit JPMS module and vendor the country cod…
dargmuesli Sep 2, 2026
9bd564a
10.0.0-RC7
dargmuesli Sep 2, 2026
e60028f
refactor(build): compile the examples as their own module (#475)
dargmuesli Sep 2, 2026
fe09274
docs(api): clarify November 2024 restriction notices (#477)
dargmuesli Sep 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,19 @@ jobs:
distribution: temurin
java-version: ${{ matrix.java }}
- name: Build with Maven
run: mvn -B package --file pom.xml
# install, so the examples below compile against this build rather than the released artifact
run: mvn -B install --file pom.xml
- name: Compile examples against the built module
# the examples are their own module, so they read the library the way a consumer does and a
# missing export fails here; clean, because an incremental build would reuse classes compiled
# against the previous descriptor and report nothing
run: mvn -B -f examples/pom.xml clean compile
- name: Check documentation consistency
run: ./ci/check-docs.py
- name: Build Javadoc
# the tag-triggered release below attaches Javadoc, so a broken reference must fail here, not after tagging
# the removal is what stops the plugin from skipping an already up-to-date output directory
run: rm -rf target/reports/apidocs && mvn -B -P javadoc-check javadoc:javadoc
- name: Upload code coverage
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
with:
Expand All @@ -53,7 +65,7 @@ jobs:
env:
GPG_KEY_NAME: B70E1EAC
GPG_PASSPHRASE_FILE: ${{ secrets.GPG_PASSPHRASE_FILE }}
GPG_PASSPHRASE_KEY: ${{ secrets.GPG_PASSPHRASE_KEY }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE_KEY }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
run: |
Expand Down
File renamed without changes.
37 changes: 37 additions & 0 deletions 404.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
permalink: /404.html
---
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page not found</title>
<script>
// Javadoc switched to its modular layout in 10.0.0-RC7, which inserted the module name into the
// path of every class page. Deep links to the old paths are in the wild, in older releases and
// in search results, so translate them here instead of serving a dead end. GitHub Pages has no
// server-side redirects, and this is one file rather than a stub per page.
(function () {
var path = location.pathname;
var moved = path
.replace(/\/apidocs\/se\/michaelthelin\/spotify\//, "/apidocs/se.michaelthelin.spotify/se/michaelthelin/spotify/")
.replace(/\/apidocs\/overview-summary\.html$/, "/apidocs/index.html");

if (moved !== path) {
location.replace(moved + location.search + location.hash);
}
})();
</script>
<style>
body { font-family: system-ui, sans-serif; margin: 4rem auto; max-width: 40rem; padding: 0 1rem; }
</style>
</head>
<body>
<h1>Page not found</h1>
<p>
If you followed a link to the API documentation, it may point at a path used before 10.0.0-RC7.
Try the <a href="/spotify-web-api-java/apidocs/">current documentation</a>.
</p>
</body>
</html>
138 changes: 138 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# AGENTS.md

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

Read [CONTRIBUTING.md](CONTRIBUTING.md) first: it carries the rules a contribution is judged against and the release procedure, and it applies to agents exactly as it does to people.
This file covers the mechanics of the codebase and does not repeat them.

## 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`
- **Build Tool:** Maven (`pom.xml`)
- **Java Version:** compiled with `--release 9`; CI builds on JDK 17 and 25

## 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).
- 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`).

### 5. Add an example and index it

Create `examples/.../<EndpointName>Example.java` and link it from the example index in `README.md`.
Mark the link with ⚠️ if the request class carries `@Deprecated`.

The examples are a separate Maven project that compiles against the installed library as its own module, which is how a missing `exports` gets caught.
Build them with `mvn -B install -DskipTests=true && mvn -B -f examples/pom.xml clean compile`; the `clean` matters, because an incremental build reuses classes compiled against the previous module descriptor.
An example may only use packages the library exports, and only types reachable through the library's own `requires transitive`.

`./ci/check-docs.py` enforces all of this and runs in CI.
It checks that every `SpotifyApi` method has an example, that example file names match their request class, that the README index is complete and its ⚠️ markers match the code, that each request's verb and path exist in `spec/openapi.yaml` with matching deprecation, and that every current-API name written in `MIGRATION.md` still resolves.

### Releasing

See the release workflow in [CONTRIBUTING.md](CONTRIBUTING.md#release-workflow-maintainers-only).
Note that the version lives in three files, and `ci/check-docs.py` fails when they disagree.

### Renaming or removing public API

Record it in `MIGRATION.md` under the relevant `#### ` heading.
Names that no longer exist have to sit in a table column whose header contains "v9", or inside a code fence under a `// v9` marker, otherwise the docs check reports them as stale.

## 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

- **Model objects are deserialized reflectively** by the shared Gson in
`model_objects/ModelObjectGson.java`, so a new field needs no parsing code.
Add `@SerializedName` only when the wire name is not the snake_case form of the field,
and add a type adapter only for a shape reflection cannot express.
- **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)`.
64 changes: 35 additions & 29 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ especially because currently only voluntary maintainers and contributors take ca
At the same time it is strongly recommended to follow certain guidelines when making contributions to open source
projects like this one. They ensure fast processing times and stable releases.

It is mandatory to stay as close to the
[Web API Endpoint Reference](https://developer.spotify.com/web-api/endpoint-reference/) as possible. Before changing
It is mandatory to stay as close to Spotify's own API description as possible.
A copy of it is vendored at `spec/openapi.yaml`, and that copy is what `ci/check-docs.py` checks a request's verb, path and deprecation against, so it is the first place to look rather than the
[Web API Endpoint Reference](https://developer.spotify.com/documentation/web-api/reference/). Before changing
anything in the code base, make sure to take one - or more - looks into related files/classes and [the project's
documentation](https://spotify-web-api-java.github.io/spotify-web-api-java/apidocs/). The whole code follows simple
and consistent code conventions for naming rules and alike. Those should be easy to grasp by comparing to the online
Expand All @@ -37,10 +38,17 @@ A complete contribution includes the following features:
Make sure that your contribution embeds well into the already existing code. For example, the unified folder/package
structure must be used wherever possible.

The library is an explicit Java module, so a class in a **new** package is unreachable for callers until that package is added to `exports` in `src/main/java/module-info.java`.
The examples are compiled as a separate module against the built library, which is what turns a forgotten `exports` into a build failure here rather than in someone else's project.

### Documentation
Document your source code! It's easy because one can most likely just stick to the online API reference. This is a
requirement. Not because your contributed source code is unreadable. Instead, the aim is to keep the Javadoc complete
and - accompanying this - to keep this project as accessible as possible to developers like yourself.
Document what a reader of the published API reference needs: types, getters and the methods on `SpotifyApi`.
Model builder setters and default constructors deliberately carry no prose, because restating the signature adds nothing that the matching getter does not already say, so Javadoc runs with `doclint` set to `all,-missing`.

What is still enforced is that every reference resolves: a dead `{@link}` fails the build, and the Javadoc step runs in CI before a tag can publish anything.

A new endpoint also needs an example under `examples/` and a link from the index in `README.md`, and a renamed or removed public name needs an entry in `MIGRATION.md`.
`ci/check-docs.py` fails without them.

### Test Coverage
Unit tests help to assure that functionality works. Due to that contribution approval requires them to be included for
Expand All @@ -49,40 +57,38 @@ none below. For arrays the length is to be checked.

#### Fixtures
A unit test builds upon JSON fixtures. For code changes, fixture files must only be updated when...
- the online examples change
- the online examples are outdated, compared against the reference description and/or the
[Web API Object Model](https://developer.spotify.com/web-api/object-model/).
- the responses Spotify actually returns change
- a fixture is outdated, compared against the schema for that response in `spec/openapi.yaml`

## Contribution Flow
1. Create a fork from this repository
2. Create a branch in your fork in which you develop your contribution (one branch per feature/fix)
3. Create meaningful and well-separated commits
4. Make sure your contribution follows the contribution guidelines above
5. Create a pull request from your feature branch to the correct branch of this project
5. Create a pull request against the branch the change belongs on:
- `main` for anything that can ship in a patch or minor release, which is most contributions: fixes, dependency updates, and new features such as an added endpoint or a new parameter
- `beta` for changes that break the published API, since those have to wait for the next major version

`beta` accumulates that next major and is released as `X.Y.Z-RCN` until it is ready, so a breaking change only reaches `main` through the release pull request. While a `beta` line is open, follow-up fixes to code that exists only there belong on `beta` too. Ask in the issue if you are unsure.

## Release Workflow (Maintainers Only)

Follow these steps to publish a new release:

### 1. Update version numbers

Update the version string in **three places**:
The version lives in **three files**:

- **`pom.xml`** — the `<version>` element near the top
- **`README.md`** — two occurrences, the Maven `<version>` snippet and the Gradle `implementation` line
- **`examples/pom.xml`** — the examples are a separate Maven project, so they carry their own copy

- **`pom.xml`** — the `<version>` element near the top of the file:
```xml
<version>X.Y.Z</version>
```
- **`README.md`** — two occurrences: the Maven `<version>` snippet and the Gradle `implementation` line:
```xml
<version>X.Y.Z</version>
```
```groovy
implementation 'se.michaelthelin.spotify:spotify-web-api-java:X.Y.Z'
```
`ci/check-docs.py` fails the build when `examples/pom.xml` falls behind `pom.xml`, so a missed bump is caught rather than shipped.

### 2. Regenerate Javadoc

The `apidocs/` directory at the project root is the published API documentation and must be regenerated on every release. The `release` Maven profile is configured to delete `apidocs/` and regenerate it.
The `apidocs/` directory at the project root is the published API documentation and must be regenerated on every release.
The `release` profile deletes and regenerates it, and because the library is an explicit module, Javadoc writes the pages under `apidocs/se.michaelthelin.spotify/`.

Run:
```bash
Expand All @@ -96,23 +102,23 @@ This will:

### 3. Commit and tag

Commit all changed files (version bumps + regenerated `apidocs/`):
Pre-releases are cut from `beta` and tagged `X.Y.Z-RCN`; stable releases are cut from `main` and tagged `X.Y.Z`. CI reacts to both patterns and to nothing else.

Commit the version bumps together with the regenerated documentation:
```bash
git add pom.xml README.md apidocs/
git add pom.xml README.md examples/pom.xml apidocs/
git commit -m "X.Y.Z"
```

Create a lightweight Git tag matching the version exactly (the CI tag pattern is `X.Y.Z` or `X.Y.Z-RCN`):
Create a lightweight tag matching the version exactly, and push the commit before the tag so CI validates the code before it publishes anything:
```bash
git tag X.Y.Z
```

Push the commit and tag:
```bash
git push origin <branch>
git push origin X.Y.Z
```

A pre-release line becomes stable by merging `beta` into `main` through the release pull request and tagging there. That merge is also what publishes the documentation, since GitHub Pages serves from `main`.

### 4. Automated release via CI

Pushing a tag triggers the GitHub Actions CI workflow (`.github/workflows/ci.yml`), which automatically:
Expand Down
Loading
Loading