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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
# Changelog

## Next release

* Add variants of watched methods that have an option to include special episodes. Deprecated the original variants.

## 6.19.0 - 2026-05-08

Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/uwetrottmann/trakt5/TraktV2.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ public class TraktV2 {
public static final String HEADER_TRAKT_API_VERSION = "trakt-api-version";
public static final String HEADER_TRAKT_API_KEY = "trakt-api-key";

public static final String QUERY_PARAM_SPECIALS = "specials";

@Nullable private OkHttpClient okHttpClient;
@Nullable private Retrofit retrofit;

Expand Down
43 changes: 43 additions & 0 deletions src/main/java/com/uwetrottmann/trakt5/enums/Specials.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright © 2026 Uwe Trottmann <uwe@uwetrottmann.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.uwetrottmann.trakt5.enums;

/**
* Option to include special episodes for the watched shows and watched episodes endpoint.
*/
public class Specials implements TraktEnum {

/**
* Return special episodes ("season 0").
* <p>
* This option is not documented, but according to the <a
* href="https://github.com/trakt/trakt-api/issues/792">GitHub issue "sync/watched/shows no returning "specials"
* episodes ?"</a> required.
*/
public static final Specials TRUE = new Specials("true");

private final String value;

private Specials(String value) {
this.value = value;
}

@Override
public String toString() {
return value;
}
}
76 changes: 76 additions & 0 deletions src/main/java/com/uwetrottmann/trakt5/services/Sync.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
import com.uwetrottmann.trakt5.enums.HistoryType;
import com.uwetrottmann.trakt5.enums.PlaybackType;
import com.uwetrottmann.trakt5.enums.RatingsFilter;
import com.uwetrottmann.trakt5.enums.Specials;
import org.threeten.bp.OffsetDateTime;
import retrofit2.Call;
import retrofit2.http.Body;
Expand Down Expand Up @@ -286,39 +287,88 @@ Call<List<BaseShow>> watchedShows(
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Like {@link Users#watchedShows(UserSlug, int, int, ExtendedShowsWatched)}.
*
* @deprecated Use {@link #watchedShows(int, int, ExtendedShowsWatched, Specials)} instead.
*/
@Deprecated
@GET("sync/watched/shows")
Call<List<BaseShow>> watchedShows(
@Query("page") int page,
@Query("limit") int limit,
@Query(value = "extended", encoded = true) ExtendedShowsWatched extended
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Like {@link Users#watchedShows(UserSlug, int, int, ExtendedShowsWatched, Specials)}.
*/
@GET("sync/watched/shows")
Call<List<BaseShow>> watchedShows(
@Query("page") int page,
@Query("limit") int limit,
@Query(value = "extended", encoded = true) ExtendedShowsWatched extended,
@Query(value = TraktV2.QUERY_PARAM_SPECIALS, encoded = true) Specials specials
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Like {@link Users#watchedShowsMin(UserSlug, int, int)}.
*
* @deprecated Use {@link #watchedShowsMin(int, int, Specials)} instead.
*/
@Deprecated
@GET("sync/watched/shows?extended=min")
Call<Map<String, Map<String, Map<String, List<OffsetDateTime>>>>> watchedShowsMin(
@Query("page") int page,
@Query("limit") int limit
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Like {@link Users#watchedShowsMin(UserSlug, int, int, Specials)}.
*/
@GET("sync/watched/shows?extended=min")
Call<Map<String, Map<String, Map<String, List<OffsetDateTime>>>>> watchedShowsMin(
@Query("page") int page,
@Query("limit") int limit,
@Query(value = TraktV2.QUERY_PARAM_SPECIALS, encoded = true) Specials specials
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Returns all episodes a user has watched.
*
* @param page Number of page of results to be returned.
* @param limit Number of results to return per page.
*
* @deprecated Use {@link #watchedEpisodes(int, int, Specials)} instead.
*/
@Deprecated
@GET("sync/watched/episodes")
Call<List<WatchedEpisode>> watchedEpisodes(
@Query("page") int page,
@Query("limit") int limit
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Returns all episodes a user has watched.
*
* @param page Number of page of results to be returned.
* @param limit Number of results to return per page.
*/
@GET("sync/watched/episodes")
Call<List<WatchedEpisode>> watchedEpisodes(
@Query("page") int page,
@Query("limit") int limit,
@Query(value = TraktV2.QUERY_PARAM_SPECIALS, encoded = true) Specials specials
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
Expand All @@ -334,13 +384,39 @@ Call<List<WatchedEpisode>> watchedEpisodes(
* ]
* }
* </pre>
*
* @deprecated Use {@link #watchedEpisodesMin(int, int, Specials)} instead.
*/
@Deprecated
@GET("sync/watched/episodes?extended=min")
Call<Map<String, List<OffsetDateTime>>> watchedEpisodesMin(
@Query("page") int page,
@Query("limit") int limit
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
* Returns episode Trakt IDs mapped to a list of watched at timestamps.
* <p>
* An example for an equivalent JSON for a single episode with three watched at timestamps:
* <pre>
* {
* "73482": [
* "2015-02-10T09:04:00.000Z",
* "2015-02-10T15:42:00.000Z",
* "2015-02-24T05:51:00.000Z"
* ]
* }
* </pre>
*/
@GET("sync/watched/episodes?extended=min")
Call<Map<String, List<OffsetDateTime>>> watchedEpisodesMin(
@Query("page") int page,
@Query("limit") int limit,
@Query(value = TraktV2.QUERY_PARAM_SPECIALS, encoded = true) Specials specials
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} required</b>
* <p>
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/com/uwetrottmann/trakt5/services/Users.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import com.uwetrottmann.trakt5.enums.ExtendedShowsWatched;
import com.uwetrottmann.trakt5.enums.HistoryType;
import com.uwetrottmann.trakt5.enums.RatingsFilter;
import com.uwetrottmann.trakt5.enums.Specials;
import org.threeten.bp.OffsetDateTime;
import retrofit2.Call;
import retrofit2.http.Body;
Expand Down Expand Up @@ -850,7 +851,10 @@ Call<List<BaseShow>> watchedShows(
* @param userSlug Example: "sean".
* @param page Number of page of results to be returned.
* @param limit Number of results to return per page.
*
* @deprecated Use {@link #watchedShows(UserSlug, int, int, ExtendedShowsWatched, Specials)} instead.
*/
@Deprecated
@GET("users/{username}/watched/shows")
Call<List<BaseShow>> watchedShows(
@Path("username") UserSlug userSlug,
Expand All @@ -859,6 +863,24 @@ Call<List<BaseShow>> watchedShows(
@Query(value = "extended", encoded = true) ExtendedShowsWatched extended
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} optional</b>
* <p>
* Returns all shows a user has watched sorted by most plays.
*
* @param userSlug Example: "sean".
* @param page Number of page of results to be returned.
* @param limit Number of results to return per page.
*/
@GET("users/{username}/watched/shows")
Call<List<BaseShow>> watchedShows(
@Path("username") UserSlug userSlug,
@Query("page") int page,
@Query("limit") int limit,
@Query(value = "extended", encoded = true) ExtendedShowsWatched extended,
@Query(value = "specials", encoded = true) Specials specials
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} optional</b>
* <p>
Expand All @@ -879,12 +901,44 @@ Call<List<BaseShow>> watchedShows(
* }
* }
* </pre>
*
* @deprecated Use {@link #watchedShowsMin(UserSlug, int, int, Specials)} instead.
*/
@Deprecated
@GET("users/{username}/watched/shows?extended=min")
Call<Map<String, Map<String, Map<String, List<OffsetDateTime>>>>> watchedShowsMin(
@Path("username") UserSlug userSlug,
@Query("page") int page,
@Query("limit") int limit
);

/**
* <b>OAuth {@link TraktV2#accessToken(String) access token} optional</b>
* <p>
* Returns show Trakt IDs mapped to season Trakt IDs, mapped to episode Trakt IDs mapped to a list of watched at
* timestamps.
* <p>
* An example for an equivalent JSON for a single show, season and episode with three watched at timestamps:
* <pre>
* {
* "77712": {
* "95726": {
* "1498291": [
* "2026-04-29T17:54:00.000Z",
* "2026-04-29T17:57:00.000Z",
* "2026-05-08T02:38:00.000Z"
* ]
* }
* }
* }
* </pre>
*/
@GET("users/{username}/watched/shows?extended=min")
Call<Map<String, Map<String, Map<String, List<OffsetDateTime>>>>> watchedShowsMin(
@Path("username") UserSlug userSlug,
@Query("page") int page,
@Query("limit") int limit,
@Query(value = TraktV2.QUERY_PARAM_SPECIALS, encoded = true) Specials specials
);

}
12 changes: 7 additions & 5 deletions src/test/java/com/uwetrottmann/trakt5/services/SyncTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import com.uwetrottmann.trakt5.enums.HistoryType;
import com.uwetrottmann.trakt5.enums.Rating;
import com.uwetrottmann.trakt5.enums.RatingsFilter;
import com.uwetrottmann.trakt5.enums.Specials;
import org.junit.Test;
import org.threeten.bp.Instant;
import org.threeten.bp.OffsetDateTime;
Expand Down Expand Up @@ -357,7 +358,7 @@ public void test_watchedMoviesMin() throws IOException {
@Test
public void test_watchedShows() throws IOException {
Response<List<BaseShow>> response = executeCallWithoutReadingBody(
getTrakt().sync().watchedShows(PAGE_ONE, LIMIT_MAX, ExtendedShowsWatched.PROGRESS));
getTrakt().sync().watchedShows(PAGE_ONE, LIMIT_MAX, ExtendedShowsWatched.PROGRESS, Specials.TRUE));

assertListPaginationHeaders(response);
assertSyncShows(response.body(), "watched");
Expand All @@ -368,15 +369,16 @@ public void test_watchedShows_extended() throws IOException {
// Note: Starting 2026-05-30 these extended values will be the default
List<BaseShow> shows = executeCall(
getTrakt().sync().watchedShows(PAGE_ONE, LIMIT_MAX,
ExtendedShowsWatched.of(ExtendedShowsWatched.FULL, ExtendedShowsWatched.NOSEASONS)));
ExtendedShowsWatched.of(ExtendedShowsWatched.FULL, ExtendedShowsWatched.NOSEASONS),
Specials.TRUE));

assertWatchedShowsFullNoSeasons(shows);
}

@Test
public void test_watchedShowsMin() throws IOException {
Response<Map<String, Map<String, Map<String, List<OffsetDateTime>>>>> response = executeCallWithoutReadingBody(
getTrakt().sync().watchedShowsMin(PAGE_ONE, LIMIT_MAX));
getTrakt().sync().watchedShowsMin(PAGE_ONE, LIMIT_MAX, Specials.TRUE));

// As of 2026-05-08, pagination is not supported, yet
// assertListPaginationHeaders(response);
Expand All @@ -387,7 +389,7 @@ public void test_watchedShowsMin() throws IOException {
@Test
public void test_watchedEpisodes() throws IOException {
Response<List<WatchedEpisode>> response = executeCallWithoutReadingBody(
getTrakt().sync().watchedEpisodes(PAGE_ONE, LIMIT_MAX));
getTrakt().sync().watchedEpisodes(PAGE_ONE, LIMIT_MAX, Specials.TRUE));

assertListPaginationHeaders(response);
assertThat(response.body())
Expand All @@ -401,7 +403,7 @@ public void test_watchedEpisodes() throws IOException {
@Test
public void test_watchedEpisodesMin() throws IOException {
Response<Map<String, List<OffsetDateTime>>> response = executeCallWithoutReadingBody(
getTrakt().sync().watchedEpisodesMin(PAGE_ONE, LIMIT_MAX));
getTrakt().sync().watchedEpisodesMin(PAGE_ONE, LIMIT_MAX, Specials.TRUE));

// As of 2026-05-08, pagination is not supported, yet
// assertListPaginationHeaders(response);
Expand Down
8 changes: 5 additions & 3 deletions src/test/java/com/uwetrottmann/trakt5/services/UsersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
import com.uwetrottmann.trakt5.enums.RatingsFilter;
import com.uwetrottmann.trakt5.enums.SortBy;
import com.uwetrottmann.trakt5.enums.SortHow;
import com.uwetrottmann.trakt5.enums.Specials;
import org.junit.Ignore;
import org.junit.Test;
import org.threeten.bp.LocalTime;
Expand Down Expand Up @@ -575,7 +576,7 @@ public void test_watchedMoviesMin() throws IOException {
public void test_watchedShows() throws IOException {
Response<List<BaseShow>> response = executeCallWithoutReadingBody(
getTrakt().users().watchedShows(TestData.USER_SLUG, PAGE_ONE, LIMIT_MAX,
ExtendedShowsWatched.PROGRESS));
ExtendedShowsWatched.PROGRESS, Specials.TRUE));

assertListPaginationHeaders(response);
assertSyncShows(response.body(), "watched");
Expand All @@ -586,15 +587,16 @@ public void test_watchedShows_extended() throws IOException {
// Note: Starting 2026-05-30 these extended values will be the default
List<BaseShow> shows = executeCall(
getTrakt().users().watchedShows(TestData.USER_SLUG, PAGE_ONE, LIMIT_MAX,
ExtendedShowsWatched.of(ExtendedShowsWatched.FULL, ExtendedShowsWatched.NOSEASONS)));
ExtendedShowsWatched.of(ExtendedShowsWatched.FULL, ExtendedShowsWatched.NOSEASONS),
Specials.TRUE));

assertWatchedShowsFullNoSeasons(shows);
}

@Test
public void test_watchedShowsMin() throws IOException {
Response<Map<String, Map<String, Map<String, List<OffsetDateTime>>>>> response = executeCallWithoutReadingBody(
getTrakt().users().watchedShowsMin(TestData.USER_SLUG, PAGE_ONE, LIMIT_MAX));
getTrakt().users().watchedShowsMin(TestData.USER_SLUG, PAGE_ONE, LIMIT_MAX, Specials.TRUE));

// As of 2026-05-08, pagination is not supported, yet
// assertListPaginationHeaders(response);
Expand Down