ShowsApi.java
/*
* tvmaze-java-client - A client to access the TVMaze API
* Copyright © 2024-2025 Andy Miles (andy.miles@amilesend.com)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.amilesend.tvmaze.client.api;
import com.amilesend.client.connection.Connection;
import com.amilesend.client.connection.RequestException;
import com.amilesend.client.parse.parser.BasicParser;
import com.amilesend.client.parse.parser.ListParser;
import com.amilesend.tvmaze.client.model.AlternateEpisode;
import com.amilesend.tvmaze.client.model.AlternateList;
import com.amilesend.tvmaze.client.model.Episode;
import com.amilesend.tvmaze.client.model.Image;
import com.amilesend.tvmaze.client.model.Season;
import com.amilesend.tvmaze.client.model.Show;
import com.amilesend.tvmaze.client.model.type.Alias;
import com.amilesend.tvmaze.client.model.type.CastMember;
import com.amilesend.tvmaze.client.model.type.CrewMember;
import com.amilesend.tvmaze.client.parse.adapters.LocalDateTypeAdapter;
import lombok.NonNull;
import okhttp3.HttpUrl;
import org.apache.commons.lang3.StringUtils;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.util.List;
/**
* TVMaze API to retrieve show information.
* <br/>
* For more information, please refer to <a href="https://www.tvmaze.com/api#shows">https://www.tvmaze.com/api#shows</a>
*/
public class ShowsApi extends ApiBase {
private static final String SHOWS_INDEX_API_PATH = "/shows";
private static final String SHOWS_API_PATH = SHOWS_INDEX_API_PATH + "/";
private static final String ALTERNATE_LISTS_API_PATH = "/alternatelists/";
private static final String SEASONS_API_PATH = "/seasons/";
private static final String ALTERNATE_EPISODES_SUB_API_PATH = "/alternateepisodes";
private static final String ALTERNATE_LISTS_SUB_API_PATH = "/alternatelists";
private static final String SEASONS_SUB_API_PATH = "/seasons";
private static final String EPISODES_SUB_API_PATH = "/episodes";
private static final String CAST_SUB_API_PATH = "/cast";
private static final String CREW_SUB_API_PATH = "/crew";
private static final String ALIASES_SUB_API_PATH = "/akas";
private static final String IMAGES_SUB_API_PATH = "/images";
/**
* Creates a new {@code ShowsApi} object.
*
* @param connection the connection
*/
public ShowsApi(final Connection connection) {
super(connection);
}
////////////
// getShow
////////////
/**
* Gets the show for the given {@code showId}.
*
* @param showId the show identifier
* @param includeEmbeddedTypes the optional embedded types to include in the show
* @return the show
* @see Show
*/
public Show getShow(final int showId, final Show.EmbeddedType... includeEmbeddedTypes) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, StringUtils.EMPTY, includeEmbeddedTypes);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new BasicParser<>(Show.class));
}
////////////////
// getEpisodes
////////////////
/**
* Gets the list of episodes for the given {@code showId}.
*
* @param showId the show identifier
* @param isSpecialsIncluded if {@code true}, include specials in the list; else {@code false}
* @return the list of episodes
*/
public List<Episode> getEpisodes(final int showId, final boolean isSpecialsIncluded) {
final HttpUrl url = validateAndFormatEpisodesUrl(showId, isSpecialsIncluded);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Episode.class));
}
private HttpUrl validateAndFormatEpisodesUrl(final int showId, final boolean isSpecialsIncluded) {
final String formattedId = validateId(showId);
final HttpUrl.Builder urlBuilder = HttpUrl.parse(
new StringBuilder(connection.getBaseUrl())
.append(SHOWS_API_PATH)
.append(formattedId)
.append("/episodes")
.toString())
.newBuilder();
if (isSpecialsIncluded) {
urlBuilder.addQueryParameter("specials", "1");
}
return urlBuilder.build();
}
//////////////////////
// getAlternateLists
//////////////////////
/**
* Gets the list of alternate episode lists for the given {@code showId} (e.g., DVD ordering).
*
* @param showId the show identifier
* @return the list of alternate episodes lists
* @see AlternateList
*/
public List<AlternateList> getAlternateLists(final int showId) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, ALTERNATE_LISTS_SUB_API_PATH);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(AlternateList.class));
}
/////////////////////
// getAlternateList
/////////////////////
/**
* Gets the alternate episode list for the given {@code alternateListId}.
*
* @param alternateListId the alternate episode list
* @param isAlternateEpisodesIncluded if {@code true}, includes the list of embedded alternate episodes; else,
* {@code false}
* @return the alternate episode list
* @see AlternateList
*/
public AlternateList getAlternateList(final int alternateListId, final boolean isAlternateEpisodesIncluded) {
final HttpUrl url = validateAndFormatAlternateListsUrl(alternateListId, isAlternateEpisodesIncluded);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new BasicParser<>(AlternateList.class));
}
private HttpUrl validateAndFormatAlternateListsUrl(
final int alternateListId,
final boolean isAlternateEpisodesIncluded) {
return isAlternateEpisodesIncluded
? validateAndFormatUrl(
ALTERNATE_LISTS_API_PATH,
alternateListId,
StringUtils.EMPTY,
AlternateList.EmbeddedType.ALTERNATE_EPISODES)
: validateAndFormatUrl(ALTERNATE_LISTS_API_PATH, alternateListId, StringUtils.EMPTY);
}
/////////////////////////
// getAlternateEpisodes
/////////////////////////
/**
* Gets the list of alternate episodes for the given {@code alternateListId}.
*
* @param alternateListId the alternate list identifier
* @param isEpisodesIncluded if {@code true}, includes the associated {@link Episode}; else, {@code false}
* @return the list of alternate episodes
* @see AlternateEpisode
*/
public List<AlternateEpisode> getAlternateEpisodes(final int alternateListId, final boolean isEpisodesIncluded) {
final HttpUrl url = validateAndFormatAlternateEpisodesUrl(alternateListId, isEpisodesIncluded);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(AlternateEpisode.class));
}
private HttpUrl validateAndFormatAlternateEpisodesUrl(
final int alternateListId,
final boolean isEpisodesIncluded) {
return isEpisodesIncluded
? validateAndFormatUrl(
ALTERNATE_LISTS_API_PATH,
alternateListId,
ALTERNATE_EPISODES_SUB_API_PATH,
AlternateList.EmbeddedType.EPISODES)
: validateAndFormatUrl(ALTERNATE_LISTS_API_PATH, alternateListId, ALTERNATE_EPISODES_SUB_API_PATH);
}
///////////////
// getEpisode
///////////////
/**
* Gets an {@code Episode} for the given show, season, and episode number.
*
* @param showId the show identifier
* @param seasonNum the season number
* @param episodeNum the associated episode number for the season
* @return the episode
* @see Episode
*/
public Episode getEpisode(final int showId, final int seasonNum, final int episodeNum) {
final HttpUrl url = validateAndFormatEpisodeUrl(showId, seasonNum, episodeNum);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new BasicParser<>(Episode.class));
}
private HttpUrl validateAndFormatEpisodeUrl(
final int showId,
final int seasonNum,
final int episodeNum) {
final String formattedShowId = validateId(showId);
final String formattedSeasonNum = validateId(seasonNum);
final String formattedEpisodeNum = validateId(episodeNum);
return HttpUrl.parse(
new StringBuilder(connection.getBaseUrl())
.append(SHOWS_API_PATH)
.append(formattedShowId)
.append("/episodebynumber")
.toString())
.newBuilder()
.addQueryParameter("season", formattedSeasonNum)
.addQueryParameter("number", formattedEpisodeNum)
.build();
}
////////////////
// getEpisodes
////////////////
/**
* Gets the list of episodes for a show that aired on the given date.
*
* @param showId the show identifier
* @param date the date of airing
* @return the list of episodes
* @see Episode
*/
public List<Episode> getEpisodes(final int showId, final LocalDate date) {
final HttpUrl url = validateAndFormatEpisodesUrl(showId, date);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Episode.class));
}
private HttpUrl validateAndFormatEpisodesUrl(final int showId, final LocalDate date) {
final String formattedShowId = validateId(showId);
final String formattedDate = validateAndFormatDate(date);
return HttpUrl.parse(
new StringBuilder(connection.getBaseUrl())
.append("/shows/")
.append(formattedShowId)
.append("/episodesbydate")
.toString())
.newBuilder()
.addQueryParameter("date", formattedDate)
.build();
}
///////////////
// getSeasons
///////////////
/**
* Gets the list of seasons for a show.
*
* @param showId the show identifier
* @return the list of seasons
* @see Season
*/
public List<Season> getSeasons(final int showId) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, SEASONS_SUB_API_PATH);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Season.class));
}
//////////////////////
// getSeasonEpisodes
//////////////////////
/**
* Gets the list of episodes for a given {@code seasonId}.
*
* @param seasonId the season identifier
* @param isGuestCastIncluded if {@code true}, returns the list of guest cast members; else {@code false}
* @return the list of episodes
* @see Episode
*/
public List<Episode> getSeasonEpisodes(final int seasonId, final boolean isGuestCastIncluded) {
final HttpUrl url = validateAndFormatSeasonEpisodesUrl(seasonId, isGuestCastIncluded);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Episode.class));
}
private HttpUrl validateAndFormatSeasonEpisodesUrl(final int seasonId, final boolean isGuestCastIncluded) {
return isGuestCastIncluded
? validateAndFormatUrl(
SEASONS_API_PATH,
seasonId,
EPISODES_SUB_API_PATH,
Episode.EmbeddedType.GUEST_CAST)
: validateAndFormatUrl(SEASONS_API_PATH, seasonId, EPISODES_SUB_API_PATH);
}
////////////
// getCast
////////////
/**
* Gets the list of cast members for a show.
*
* @param showId the show identifier
* @return the list of cast members
* @see CastMember
*/
public List<CastMember> getCast(final int showId) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, CAST_SUB_API_PATH);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(CastMember.class));
}
////////////
// getCrew
////////////
/**
* Gets the list of crew members for a show.
*
* @param showId the show identifier
* @return the list of crew members
* @see CrewMember
*/
public List<CrewMember> getCrew(final int showId) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, CREW_SUB_API_PATH);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(CrewMember.class));
}
///////////////
// getAliases
///////////////
/**
* Gets the list of alternative show names, or aliases.
*
* @param showId the show identifier
* @return the list of aliases
* @see Alias
*/
public List<Alias> getAliases(final int showId) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, ALIASES_SUB_API_PATH);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Alias.class));
}
//////////////
// getImages
//////////////
/**
* Gets the list of images for a show.
*
* @param showId the show identifier
* @return the list of images
* @see Image
*/
public List<Image> getImages(final int showId) {
final HttpUrl url = validateAndFormatUrl(SHOWS_API_PATH, showId, IMAGES_SUB_API_PATH);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Image.class));
}
/////////////
// getIndex
/////////////
/**
* Gets the list of all shows in the TVMaze database. Note: This is paginated and requires manual specification
* of the page number with a maximum of 250 shows per response. This operation will throw a {@link RequestException}
* when no more pages exist.
*
* @param pageNum the page number
* @return the list of shows
* @throws RequestException if there are no more shows to return
*/
public List<Show> getIndex(final int pageNum) {
final HttpUrl url = validateAndFormatIndexUrl(SHOWS_INDEX_API_PATH, pageNum);
return connection.execute(
connection.newRequestBuilder()
.url(url)
.build(),
new ListParser<>(Show.class));
}
private static String validateAndFormatDate(@NonNull final LocalDate date) {
return URLEncoder.encode(date.format(LocalDateTypeAdapter.FORMATTER), StandardCharsets.UTF_8);
}
}