Skip to content

Commit

Permalink
Replace web browser with okhttp3 client (#155)
Browse files Browse the repository at this point in the history
* Make all functions that call the api throw an exception if it fails
* Better error handling
* Replace WebBrowser with an OkHttp3 Client
* Can pass api key in header instead of storing in TmdbApi
* Better performance and overall a better way to go about it
* Add ObjectReader to AbstractTmdbApi for better performance when reading ResponseStatus and checking for errors
* Add TmdbResponseCode enum for tmdb responses
  • Loading branch information
c-eg authored Jan 9, 2024
1 parent 51f11af commit ec54847
Show file tree
Hide file tree
Showing 37 changed files with 462 additions and 602 deletions.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ dependencies {
testCompileOnly 'org.projectlombok:lombok:1.18.30'
testAnnotationProcessor 'org.projectlombok:lombok:1.18.30'

implementation 'com.squareup.okhttp3:okhttp:4.12.0'

implementation 'com.fasterxml.jackson.core:jackson-annotations:2.16.0'
implementation 'com.fasterxml.jackson.core:jackson-core:2.16.0'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.0'
Expand Down
2 changes: 2 additions & 0 deletions config/checkstyle/suppressions.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@
<suppress files="info.movito.themoviedbapi.model" checks="JavadocMethod"/>

<suppress files="info.movito.themoviedbapi.tools.MovieDbException" checks="JavadocMethod"/>
<suppress files="info.movito.themoviedbapi.tools.TmdbException" checks="JavadocMethod"/>
<suppress files="info.movito.themoviedbapi.tools.TmdbException" checks="MissingJavadocMethod"/>
</suppressions>
74 changes: 39 additions & 35 deletions src/main/java/info/movito/themoviedbapi/AbstractTmdbApi.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
package info.movito.themoviedbapi;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import info.movito.themoviedbapi.model.core.ResponseStatus;
import info.movito.themoviedbapi.model.core.ResponseStatusException;
import com.fasterxml.jackson.databind.ObjectReader;
import info.movito.themoviedbapi.model.core.responses.ResponseStatus;
import info.movito.themoviedbapi.model.core.responses.TmdbResponseException;
import info.movito.themoviedbapi.tools.ApiUrl;
import info.movito.themoviedbapi.tools.MovieDbException;
import info.movito.themoviedbapi.tools.RequestType;
import info.movito.themoviedbapi.tools.TmdbResponseCode;
import lombok.AccessLevel;
import lombok.Getter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import static info.movito.themoviedbapi.tools.TmdbResponseCode.REQUEST_LIMIT_EXCEEDED;

/**
* Class to be inherited by a TmdbApi class.
Expand All @@ -30,14 +32,11 @@ public abstract class AbstractTmdbApi {

public static final String PARAM_API_KEY = "api_key";

protected static final ObjectMapper jsonMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
@Getter(AccessLevel.PROTECTED)
private static final ObjectMapper objectMapper = new ObjectMapper()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

// see https://www.themoviedb.org/documentation/api/status-codes
private static final Collection<Integer> SUCCESS_STATUS_CODES = Arrays.asList(
1, // Success
12, // The item/record was updated successfully.
13 // The item/record was updated successfully.
);
private static final ObjectReader responseStatusReader = objectMapper.readerFor(ResponseStatus.class);

protected final TmdbApi tmdbApi;

Expand All @@ -55,55 +54,60 @@ public abstract class AbstractTmdbApi {
* @param <T> the type of class to map to
* @return the mapped class
*/
public <T> T mapJsonResult(ApiUrl apiUrl, Class<T> clazz) {
return mapJsonResult(apiUrl, clazz, null);
public <T> T mapJsonResult(ApiUrl apiUrl, Class<T> clazz) throws TmdbResponseException {
return mapJsonResult(apiUrl, null, clazz);
}

/**
* Maps a json result to a class.
*
* @param apiUrl the api url to map
* @param clazz the class to map to
* @param jsonBody the json body
* @param clazz the class to map to
* @param <T> the type of class to map to
* @return the mapped class.
*/
public <T> T mapJsonResult(ApiUrl apiUrl, Class<T> clazz, String jsonBody) {
return mapJsonResult(apiUrl, clazz, jsonBody, RequestType.GET);
public <T> T mapJsonResult(ApiUrl apiUrl, String jsonBody, Class<T> clazz) throws TmdbResponseException {
return mapJsonResult(apiUrl, jsonBody, RequestType.GET, clazz);
}

/**
* Maps a json result to a class.
*
* @param apiUrl the api url to map
* @param clazz the class to map to
* @param jsonBody the json body
* @param requestType the type of request
* @param clazz the class to map to
* @param <T> the type of class to map to
* @return the mapped class.
*/
public <T> T mapJsonResult(ApiUrl apiUrl, Class<T> clazz, String jsonBody, RequestType requestType) {
String webpage = tmdbApi.requestWebPage(apiUrl, jsonBody, requestType);
public <T> T mapJsonResult(ApiUrl apiUrl, String jsonBody, RequestType requestType, Class<T> clazz) throws TmdbResponseException {
String jsonResponse = tmdbApi.getTmdbUrlReader().readUrl(apiUrl.buildUrl(), jsonBody, requestType);

try {
// check if was error responseStatus
ResponseStatus responseStatus = jsonMapper.readValue(webpage, ResponseStatus.class);
// work around the problem that there's no status code for suspected spam names yet
String suspectedSpam = "Unable to create list because: Description is suspected to be spam.";
if (webpage.contains(suspectedSpam)) {
responseStatus = new ResponseStatus(-100, suspectedSpam);
}

// if null, the json response was not a error responseStatus code, but something else
// check if the response was successful. tmdb have their own codes for successful and unsuccessful responses.
// some 2xx codes are not successful. See: https://developer.themoviedb.org/docs/errors for more info.
ResponseStatus responseStatus = responseStatusReader.readValue(jsonResponse);
Integer statusCode = responseStatus.getStatusCode();
if (statusCode != null && !SUCCESS_STATUS_CODES.contains(statusCode)) {
throw new ResponseStatusException(responseStatus);

if (statusCode != null) {
TmdbResponseCode tmdbResponseCode = TmdbResponseCode.fromCode(statusCode);

if (tmdbResponseCode != null) {
if (REQUEST_LIMIT_EXCEEDED == tmdbResponseCode) {
Thread.sleep(1000);
return mapJsonResult(apiUrl, jsonBody, requestType, clazz);
}
else if (!tmdbResponseCode.isSuccess()) {
throw new TmdbResponseException(tmdbResponseCode);
}
}
}

return jsonMapper.readValue(webpage, clazz);
return objectMapper.readValue(jsonResponse, clazz);
}
catch (IOException ex) {
throw new MovieDbException("mapping failed:\n" + webpage, ex);
catch (JsonProcessingException | InterruptedException exception) {
throw new TmdbResponseException(exception);
}
}
}
59 changes: 31 additions & 28 deletions src/main/java/info/movito/themoviedbapi/TmdbAccount.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import info.movito.themoviedbapi.model.config.Account;
import info.movito.themoviedbapi.model.core.AccountID;
import info.movito.themoviedbapi.model.core.MovieResultsPage;
import info.movito.themoviedbapi.model.core.ResponseStatus;
import info.movito.themoviedbapi.model.core.responses.ResponseStatus;
import info.movito.themoviedbapi.model.core.ResultsPage;
import info.movito.themoviedbapi.model.core.SessionToken;
import info.movito.themoviedbapi.model.core.responses.TmdbResponseException;
import info.movito.themoviedbapi.tools.ApiUrl;
import info.movito.themoviedbapi.tools.MovieDbException;
import info.movito.themoviedbapi.tools.TmdbResponseCode;

import java.util.Collections;
import java.util.HashMap;
Expand Down Expand Up @@ -36,7 +38,7 @@ public class TmdbAccount extends AbstractTmdbApi {
/**
* Get the basic information for an account. You will need to have a valid session id.
*/
public Account getAccount(SessionToken sessionToken) {
public Account getAccount(SessionToken sessionToken) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT);

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -48,7 +50,7 @@ public Account getAccount(SessionToken sessionToken) {
* Get the lists that as user has created.
*/
public MovieListResultsPage getLists(SessionToken sessionToken, AccountID accountId, String language,
Integer page) {
Integer page) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "lists");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -61,7 +63,7 @@ public MovieListResultsPage getLists(SessionToken sessionToken, AccountID accoun
/**
* Get the rated movies from the account.
*/
public MovieResultsPage getRatedMovies(SessionToken sessionToken, AccountID accountId, Integer page) {
public MovieResultsPage getRatedMovies(SessionToken sessionToken, AccountID accountId, Integer page) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "rated/movies");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -73,7 +75,7 @@ public MovieResultsPage getRatedMovies(SessionToken sessionToken, AccountID acco
/**
* Get the rated tv shows from the account.
*/
public TvResultsPage getRatedTvSeries(SessionToken sessionToken, AccountID accountId, Integer page) {
public TvResultsPage getRatedTvSeries(SessionToken sessionToken, AccountID accountId, Integer page) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "rated/tv");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -85,7 +87,8 @@ public TvResultsPage getRatedTvSeries(SessionToken sessionToken, AccountID accou
/**
* Get the rated tv episodes from the account.
*/
public TvEpisodesResultsPage getRatedEpisodes(SessionToken sessionToken, AccountID accountId, Integer page) {
public TvEpisodesResultsPage getRatedEpisodes(SessionToken sessionToken, AccountID accountId, Integer page)
throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "rated/tv/episodes");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -99,7 +102,7 @@ public TvEpisodesResultsPage getRatedEpisodes(SessionToken sessionToken, Account
* <p>
* A valid session id is required.
*/
public boolean postMovieRating(SessionToken sessionToken, Integer movieId, Integer rating) {
public boolean postMovieRating(SessionToken sessionToken, Integer movieId, Integer rating) throws TmdbResponseException {
return postRatingInternal(sessionToken, rating, new ApiUrl(TmdbMovies.TMDB_METHOD_MOVIE, movieId, "rating"));
}

Expand All @@ -108,15 +111,15 @@ public boolean postMovieRating(SessionToken sessionToken, Integer movieId, Integ
* <p>
* A valid session id is required.
*/
public boolean postTvSeriesRating(SessionToken sessionToken, Integer movieId, Integer rating) {
public boolean postTvSeriesRating(SessionToken sessionToken, Integer movieId, Integer rating) throws TmdbResponseException {
return postRatingInternal(sessionToken, rating, new ApiUrl(TmdbTV.TMDB_METHOD_TV, movieId, "rating"));
}

/**
* This method lets users rate a tv episode.
*/
public boolean postTvExpisodeRating(SessionToken sessionToken, Integer seriesId, Integer seasonNumber,
Integer episodeNumber, Integer rating) {
Integer episodeNumber, Integer rating) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(
TMDB_METHOD_TV, seriesId,
TMDB_METHOD_TV_SEASON, seasonNumber,
Expand All @@ -127,22 +130,22 @@ public boolean postTvExpisodeRating(SessionToken sessionToken, Integer seriesId,
return postRatingInternal(sessionToken, rating, apiUrl);
}

private boolean postRatingInternal(SessionToken sessionToken, Integer rating, ApiUrl apiUrl) {
private boolean postRatingInternal(SessionToken sessionToken, Integer rating, ApiUrl apiUrl) throws TmdbResponseException {
apiUrl.addPathParam(PARAM_SESSION, sessionToken);

if (rating < 0 || rating > 10) {
throw new MovieDbException("rating out of range");
}

String jsonBody = Utils.convertToJson(jsonMapper, Collections.singletonMap("value", rating));
String jsonBody = Utils.convertToJson(getObjectMapper(), Collections.singletonMap("value", rating));

return mapJsonResult(apiUrl, ResponseStatus.class, jsonBody).getStatusCode() == 12;
return mapJsonResult(apiUrl, jsonBody, ResponseStatus.class).getStatusCode() == TmdbResponseCode.ITEM_UPDATED.getTmdbCode();
}

/**
* Get favourites movies from the account.
*/
public MovieResultsPage getFavoriteMovies(SessionToken sessionToken, AccountID accountId) {
public MovieResultsPage getFavoriteMovies(SessionToken sessionToken, AccountID accountId) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "favorite/movies");
apiUrl.addPathParam(PARAM_SESSION, sessionToken);

Expand All @@ -152,7 +155,7 @@ public MovieResultsPage getFavoriteMovies(SessionToken sessionToken, AccountID a
/**
* Get the favorite tv shows from the account.
*/
public TvResultsPage getFavoriteSeries(SessionToken sessionToken, AccountID accountId, Integer page) {
public TvResultsPage getFavoriteSeries(SessionToken sessionToken, AccountID accountId, Integer page) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "favorite/tv");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -165,41 +168,41 @@ public TvResultsPage getFavoriteSeries(SessionToken sessionToken, AccountID acco
* Remove a movie from an account's favorites list.
*/
public ResponseStatus addFavorite(SessionToken sessionToken, AccountID accountId, Integer movieId,
MediaType mediaType) {
MediaType mediaType) throws TmdbResponseException {
return changeFavoriteStatus(sessionToken, accountId, movieId, mediaType, true);
}

/**
* Remove a movie from an account's favorites list.
*/
public ResponseStatus removeFavorite(SessionToken sessionToken, AccountID accountId, Integer movieId,
MediaType mediaType) {
MediaType mediaType) throws TmdbResponseException {
return changeFavoriteStatus(sessionToken, accountId, movieId, mediaType, false);
}

private ResponseStatus changeFavoriteStatus(SessionToken sessionToken, AccountID accountId, Integer movieId,
MediaType mediaType, boolean isFavorite) {
MediaType mediaType, boolean isFavorite) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "favorite");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);

HashMap<String, Object> body = new HashMap<String, Object>();
HashMap<String, Object> body = new HashMap<>();

body.put("media_type", mediaType.toString());
body.put("media_id", movieId);
body.put("favorite", isFavorite);

String jsonBody = Utils.convertToJson(jsonMapper, body);
String jsonBody = Utils.convertToJson(getObjectMapper(), body);

return mapJsonResult(apiUrl, ResponseStatus.class, jsonBody);
return mapJsonResult(apiUrl, jsonBody, ResponseStatus.class);
}

/**
* Get the list of movies on an accounts watchlist.
*
* @return The watchlist of the user
*/
public MovieResultsPage getWatchListMovies(SessionToken sessionToken, AccountID accountId, Integer page) {
public MovieResultsPage getWatchListMovies(SessionToken sessionToken, AccountID accountId, Integer page) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "watchlist/movies");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);
Expand All @@ -213,7 +216,7 @@ public MovieResultsPage getWatchListMovies(SessionToken sessionToken, AccountID
*
* @return The watchlist of the user
*/
public TvResultsPage getWatchListSeries(SessionToken sessionToken, AccountID accountId, Integer page) {
public TvResultsPage getWatchListSeries(SessionToken sessionToken, AccountID accountId, Integer page) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "watchlist/tv");
apiUrl.addPathParam(PARAM_SESSION, sessionToken);

Expand All @@ -226,33 +229,33 @@ public TvResultsPage getWatchListSeries(SessionToken sessionToken, AccountID acc
* Add a movie to an account's watch list.
*/
public ResponseStatus addToWatchList(SessionToken sessionToken, AccountID accountId, Integer movieId,
MediaType mediaType) {
MediaType mediaType) throws TmdbResponseException {
return modifyWatchList(sessionToken, accountId, movieId, mediaType, true);
}

/**
* Remove a movie from an account's watch list.
*/
public ResponseStatus removeFromWatchList(SessionToken sessionToken, AccountID accountId, Integer movieId,
MediaType mediaType) {
MediaType mediaType) throws TmdbResponseException {
return modifyWatchList(sessionToken, accountId, movieId, mediaType, false);
}

private ResponseStatus modifyWatchList(SessionToken sessionToken, AccountID accountId, Integer movieId,
MediaType mediaType, boolean isWatched) {
MediaType mediaType, boolean isWatched) throws TmdbResponseException {
ApiUrl apiUrl = new ApiUrl(TMDB_METHOD_ACCOUNT, accountId, "watchlist");

apiUrl.addPathParam(PARAM_SESSION, sessionToken);

HashMap<String, Object> body = new HashMap<String, Object>();
HashMap<String, Object> body = new HashMap<>();

body.put("media_type", mediaType.toString());
body.put("media_id", movieId);
body.put("watchlist", isWatched);

String jsonBody = Utils.convertToJson(jsonMapper, body);
String jsonBody = Utils.convertToJson(getObjectMapper(), body);

return mapJsonResult(apiUrl, ResponseStatus.class, jsonBody);
return mapJsonResult(apiUrl, jsonBody, ResponseStatus.class);
}

/**
Expand Down
Loading

0 comments on commit ec54847

Please sign in to comment.