Implement MovieUtil and alter corresponding DiscoverTask and DetailFragment. Change Movie to MovieInfo with Builder Pattern.

This commit is contained in:
Aargonian
2016-02-25 15:14:23 -05:00
parent 2792853cb2
commit f076a75161
7 changed files with 239 additions and 333 deletions

View File

@@ -51,8 +51,8 @@ public class TestDb extends AndroidTestCase {
} while( c.moveToNext() );
// if this fails, it means that your database doesn't contain both the location entry
// and Movie entry tables
assertTrue("Error: Your database was created without the Movie Table",
// and MovieInfo entry tables
assertTrue("Error: Your database was created without the MovieInfo Table",
tableNameHashSet.isEmpty());
// now, do our tables contain the correct columns?
@@ -102,7 +102,7 @@ public class TestDb extends AndroidTestCase {
Cursor cursor = db.query(MovieContract.MovieEntry.TABLE_NAME,
null, null, null, null, null, null);
assertTrue("Error: No Movies Returned From Query!", cursor.moveToFirst());
TestUtilities.validateCurrentRecord("Error: Movie Not Validated!", cursor, testValues);
TestUtilities.validateCurrentRecord("Error: MovieInfo Not Validated!", cursor, testValues);
//Test bitmap
String f =cursor.getString(cursor.getColumnIndex(MovieContract.MovieEntry.COLUMN_IMG_PATH));

View File

@@ -45,7 +45,7 @@ public class TestProvider extends AndroidTestCase {
}
Log.e(LOG_TAG, "SUSPECT FAVORITE? " + cursor.getInt(cursor.getColumnIndex(MovieEntry.COLUMN_FAVORITE)));
}
assertEquals("Error: Movie Records Not Deleted!", 0, cursor.getCount());
assertEquals("Error: MovieInfo Records Not Deleted!", 0, cursor.getCount());
cursor.close();
}
@@ -116,7 +116,7 @@ public class TestProvider extends AndroidTestCase {
Uri movieUri = mContext.getContentResolver().insert(MovieEntry.CONTENT_URI, values);
long movieTMDBId = ContentUris.parseId(movieUri);
Log.d(LOG_TAG, "Movie ID: " + movieTMDBId);
Log.d(LOG_TAG, "MovieInfo ID: " + movieTMDBId);
ContentValues updatedValues = new ContentValues(values);
updatedValues.put(MovieEntry.COLUMN_POPULARITY, 25.3);
@@ -202,7 +202,7 @@ public class TestProvider extends AndroidTestCase {
for ( int i = 0; i < BULK_INSERT_RECORDS_TO_INSERT; i++) {
ContentValues MovieValues = new ContentValues();
MovieValues.put(MovieContract.MovieEntry.COLUMN_TMDB_ID, startID++);
MovieValues.put(MovieContract.MovieEntry.COLUMN_TITLE, "Game Grumps: The Movie");
MovieValues.put(MovieContract.MovieEntry.COLUMN_TITLE, "Game Grumps: The MovieInfo");
MovieValues.put(MovieContract.MovieEntry.COLUMN_DESC, "Two men, one game, 10 minutes");
MovieValues.put(MovieContract.MovieEntry.COLUMN_POPULARITY, 9001);
MovieValues.put(MovieContract.MovieEntry.COLUMN_IMG_PATH, movie);

View File

@@ -3,7 +3,6 @@ package com.example.android.popularmovies;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
@@ -15,8 +14,7 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.example.android.popularmovies.data.FileUtils;
import com.example.android.popularmovies.data.MovieContract.MovieEntry;
import com.example.android.popularmovies.data.MovieUtil;
import butterknife.Bind;
import butterknife.ButterKnife;
@@ -34,31 +32,10 @@ public class DetailFragment extends Fragment
@Bind(R.id.titleView) TextView title;
@Bind(R.id.posterImage) ImageView poster;
private Movie movie;
private MovieInfo movie;
private boolean viewCreated;
private boolean displayTitle = false;
private final String[] projection = {
MovieEntry.COLUMN_TITLE,
MovieEntry.COLUMN_DESC,
MovieEntry.COLUMN_IMG_PATH,
MovieEntry.COLUMN_RELEASE,
MovieEntry.COLUMN_RUNTIME,
MovieEntry.COLUMN_RATING,
MovieEntry.COLUMN_VOTE_CNT,
MovieEntry.COLUMN_GENRES
};
@SuppressWarnings("all") private final int TITLE = 0;
@SuppressWarnings("all") private final int OVERVIEW = 1;
@SuppressWarnings("all") private final int IMG_PATH = 2;
@SuppressWarnings("all") private final int RELEASE = 3;
@SuppressWarnings("all") private final int RUNTIME = 4;
@SuppressWarnings("all") private final int RATING = 5;
@SuppressWarnings("all") private final int VOTE_COUNT = 6;
@SuppressWarnings("all") private final int GENRES = 7;
@Override
public void onAttach(Context context)
{
@@ -74,16 +51,15 @@ public class DetailFragment extends Fragment
Intent intent = this.getActivity().getIntent();
if(savedInstanceState != null)
{
movie = new Movie(
savedInstanceState.getString("MOVIE_TITLE"),
savedInstanceState.getString("MOVIE_OVERVIEW"),
(Bitmap)savedInstanceState.getParcelable("MOVIE_POSTER"),
savedInstanceState.getString("MOVIE_RELEASE"),
savedInstanceState.getInt("MOVIE_RUNTIME"),
savedInstanceState.getDouble("MOVIE_RATING"),
savedInstanceState.getInt("MOVIE_VOTES"),
savedInstanceState.getStringArray("MOVIE_GENRES")
);
movie = MovieInfo.buildMovie()
.withTitle(savedInstanceState.getString("MOVIE_TITLE"))
.withOverview(savedInstanceState.getString("MOVIE_OVERVIEW"))
.withPoster((Bitmap)savedInstanceState.getParcelable("MOVIE_POSTER"))
.withReleaseDate(savedInstanceState.getString("MOVIE_RELEASE"))
.withRuntime(savedInstanceState.getInt("MOVIE_RUNTIME"))
.withRating(savedInstanceState.getDouble("MOVIE_RATING"))
.withVoteCount(savedInstanceState.getInt("MOVIE_VOTES"))
.withGenres(savedInstanceState.getStringArray("MOVIE_GENRES")).build();
} else {
Long id = intent.getLongExtra("MOVIE_ID", -1);
if(id != -1)
@@ -102,8 +78,8 @@ public class DetailFragment extends Fragment
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Log.d(LOG_TAG, "DETAIL FRAGMENT VIEW BEING CREATED");
Bundle savedInstanceState)
{
View root = inflater.inflate(R.layout.fragment_detail, container, false);
ButterKnife.bind(this, root);
if(!displayTitle)
@@ -112,16 +88,12 @@ public class DetailFragment extends Fragment
if(movie != null) {
setMovieInfo(movie);
} else {
movie = new Movie(
"Pick a Movie!",
"You can browse movies in the list and touch one to see its details here!",
BitmapFactory.decodeResource(this.getResources(), R.drawable.noimage),
"",
0,
10.0,
0,
new String[]{"None"}
);
movie = MovieInfo.buildMovie().withTitle("Pick a Movie!")
.withOverview("You can browse movies in the list and " +
"touch one to see its details here!")
.withPoster(
BitmapFactory.decodeResource(this.getResources(), R.drawable.noimage))
.build();
setMovieInfo(movie);
}
return root;
@@ -166,31 +138,11 @@ public class DetailFragment extends Fragment
public void setMovie(Long id) {
Cursor cur = getActivity().getContentResolver().query(MovieEntry.buildMovieUriWithId(id),
projection, null, null, null);
if (cur == null || !cur.moveToFirst())
return;
movie = new Movie(
cur.getString(TITLE),
cur.getString(OVERVIEW),
FileUtils.getImage(cur.getString(IMG_PATH)),
cur.getString(RELEASE),
cur.getInt(RUNTIME),
cur.getDouble(RATING),
cur.getInt(VOTE_COUNT),
parseGenres(cur.getString(GENRES))
);
cur.close();
setMovieInfo(movie);
}
private String[] parseGenres(String genres)
{
return genres.split("_");
setMovieInfo(MovieUtil.getMovie(getActivity(), id));
}
@SuppressLint("SetTextI18n")
private void setMovieInfo(Movie movie)
private void setMovieInfo(MovieInfo movie)
{
this.movie = movie;
if(viewCreated) {

View File

@@ -25,6 +25,7 @@ public class DiscoverFragment extends Fragment implements MovieAdapter.MovieItem
//Neglecting use of ButterKnife for this fragment, only one findViewById() is used.
private static final String LOG_TAG = DiscoverFragment.class.getSimpleName();
private static final Integer PREFERRED_COLUMN_WIDTH = 100;
private static final Integer MOVIE_LOADER = 0;
private OnMovieSelectedListener mListener;
private RecyclerView mMoviesGridView;
private MovieAdapter mAdapter;

View File

@@ -1,22 +1,19 @@
package com.example.android.popularmovies;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.util.Log;
import com.example.android.popularmovies.data.FileUtils;
import com.example.android.popularmovies.data.MovieContract;
import com.example.android.popularmovies.data.MovieUtil;
import com.nytegear.android.network.NetworkUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
@@ -25,15 +22,14 @@ import java.net.URL;
*
* The DiscoverTask is responsible for fetching movies from theMovieDb's /discover/movie endpoint.
* What the task does is it first downloads a list of movies at the specified page from the endpoint
* Once the movies have been pulled down, it parses each id in the list, and checks to see if that
* movie already exists in the database. If it doesn't, it silently downloads the movie and puts it
* in the database. Either way, it then calls the publicProgress method with the TMDB_ID of the
* movie, which can be used to fetch it from the DB for any activity/fragment listening.
* Once the movies have been pulled down, it parses each id in the list, and hands it to MovieUtil.
* MovieUtil will either download the movie or retrieve it from the database if it is already cached.
* Once the movie has been retrieved, DiscoverTask then calls the publicProgress method with the
* TMDB_ID of the movie.
*/
public class DiscoverTask extends AsyncTask<Integer, String, Void>
{
private final String LOG_TAG = DiscoverTask.class.getSimpleName();
private final String TMDB_DISCOVER_URL = Movie.TMDB_URL_BASE + "/discover/movie?";
private Context mContext;
public DiscoverTask(Context context) {
@@ -57,8 +53,6 @@ public class DiscoverTask extends AsyncTask<Integer, String, Void>
private void getMoviesWithNetworkAvailable(int page)
{
Log.d(LOG_TAG, "Getting Movies with Available Network!");
//Get Sort Parameter
String SORT = Utility.getSort(mContext);
if(SORT.equalsIgnoreCase(mContext.getString(R.string.pref_sort_popularity_value)))
SORT = "popularity.desc";
@@ -69,9 +63,10 @@ public class DiscoverTask extends AsyncTask<Integer, String, Void>
try
{
String TMDB_DISCOVER_URL = MovieUtil.TMDB_URL_BASE + "/discover/movie?";
Uri.Builder builder = Uri.parse(TMDB_DISCOVER_URL).buildUpon()
.appendQueryParameter("sort_by", SORT)
.appendQueryParameter("api_key", Movie.API_KEY)
.appendQueryParameter("api_key", MovieUtil.API_KEY)
.appendQueryParameter("page", Integer.toString(page));
//Hack to avoid strange results in vote average sorts.
@@ -81,13 +76,12 @@ public class DiscoverTask extends AsyncTask<Integer, String, Void>
Uri uri = builder.build();
String moviesJSON = NetworkUtil.getURL(new URL(uri.toString()).toString());
JSONObject movieList;
if(moviesJSON != null)
movieList = new JSONObject(moviesJSON);
else {
Log.e(LOG_TAG, "Catastrophe! We were unable to get the movies from " +
Movie.TMDB_URL_BASE + "! Retrieving movies from local database only.");
MovieUtil.TMDB_URL_BASE + "! Retrieving movies from local database only.");
getMoviesWithoutNetwork(page);
return;
}
@@ -96,48 +90,14 @@ public class DiscoverTask extends AsyncTask<Integer, String, Void>
JSONArray results = movieList.getJSONArray("results");
for(int i = 0; i < results.length(); i++)
{
Integer id = results.getJSONObject(i).getInt("id");
//Intentional violation of SRP here, to avoid querying the database twice for
//imgPath. Instead, we can use the path itself to determine if it was in the db.
String imgFilePath = existsInDatabase(id);
if(imgFilePath == null) {
Movie movie = Movie.getMovie(id, mContext);
if(movie == null)
continue;
ContentValues values = new ContentValues();
try {
imgFilePath = FileUtils.storeImage(mContext,
Movie.getPoster(mContext,
results.getJSONObject(i).getString("poster_path")),
id + ".png");
} catch (IOException ex) {
Log.e(LOG_TAG, "Error Getting/Storing Poster: " + ex.getMessage(), ex);
imgFilePath = FileUtils.storeImage(
mContext,
BitmapFactory.decodeResource(mContext.getResources(),
R.drawable.noimage),
id + ".png");
}
values.put(MovieContract.MovieEntry.COLUMN_TMDB_ID, id);
values.put(MovieContract.MovieEntry.COLUMN_DESC, movie.getOverview());
values.put(MovieContract.MovieEntry.COLUMN_IMG_PATH, imgFilePath);
values.put(MovieContract.MovieEntry.COLUMN_RATING, movie.getRating());
values.put(MovieContract.MovieEntry.COLUMN_RELEASE, movie.getReleaseDate());
values.put(MovieContract.MovieEntry.COLUMN_POPULARITY,
results.getJSONObject(i).getDouble("popularity"));
values.put(MovieContract.MovieEntry.COLUMN_RUNTIME, movie.getRunTime());
values.put(MovieContract.MovieEntry.COLUMN_TITLE, movie.getTitle());
values.put(MovieContract.MovieEntry.COLUMN_VOTE_CNT, movie.getVoteCount());
values.put(MovieContract.MovieEntry.COLUMN_GENRES,
parseGenres(movie.getGenres()));
mContext.getContentResolver().insert(MovieContract.MovieEntry.CONTENT_URI,
values);
} else {
Log.v(LOG_TAG, "IT'S IN THE DATABASE! WHOO!");
Long id = results.getJSONObject(i).getLong("id");
//Use the 'FromNet' version of GetMovie so we can update any relevant info as well.
MovieInfo movie = MovieUtil.getMovieFromNet(mContext, id);
if(movie == null) {
Log.w(LOG_TAG, "NULL MOVIE: " + id);
continue;
}
publishProgress(Long.toString(id), imgFilePath);
publishProgress(Long.toString(id), movie.getPosterPath());
}
} catch (MalformedURLException ex) {
Log.e(LOG_TAG, "Malformed URL: " + ex.getMessage(), ex);
@@ -146,41 +106,6 @@ public class DiscoverTask extends AsyncTask<Integer, String, Void>
}
}
private String parseGenres(String[] genres)
{
StringBuilder genreEntry = new StringBuilder();
for(int i = 0; i < genres.length; i++)
{
genreEntry.append(genres[i]);
if(i != genres.length-1)
genreEntry.append('_');
}
return genreEntry.toString();
}
private String existsInDatabase(int id)
{
Cursor cursor =
mContext.getContentResolver().query(
MovieContract.MovieEntry.CONTENT_URI,
null,
MovieContract.MovieEntry.COLUMN_TMDB_ID + " = ?",
new String[]{Long.toString(id)},
null
);
if(cursor == null || !cursor.moveToFirst()) {
if(cursor != null)
cursor.close();
return null;
} else {
String path = cursor.getString(
cursor.getColumnIndex(MovieContract.MovieEntry.COLUMN_IMG_PATH));
cursor.close();
return path;
}
}
private void getMoviesWithoutNetwork(int page) {
long startRow = page * 25;
long endRow = (page + 1) * 25;
@@ -202,14 +127,21 @@ public class DiscoverTask extends AsyncTask<Integer, String, Void>
sort
);
if(!(cursor == null || !cursor.moveToFirst())) {
int idIndex = cursor.getColumnIndex(MovieContract.MovieEntry.COLUMN_TMDB_ID);
int imgIndex = cursor.getColumnIndex(MovieContract.MovieEntry.COLUMN_IMG_PATH);
for(int i = 0; i < cursor.getCount(); i++)
{
publishProgress(Long.toString(cursor.getLong(idIndex)), cursor.getString(imgIndex));
cursor.moveToNext();
}
if(cursor == null) {
Log.w(LOG_TAG, "NULL CURSOR!");
return;
} else if (!cursor.moveToFirst()) {
Log.w(LOG_TAG, "No rows returned from cursor!");
cursor.close();
return;
}
int idIndex = cursor.getColumnIndex(MovieContract.MovieEntry.COLUMN_TMDB_ID);
int imgIndex = cursor.getColumnIndex(MovieContract.MovieEntry.COLUMN_IMG_PATH);
for(int i = 0; i < cursor.getCount(); i++)
{
publishProgress(Long.toString(cursor.getLong(idIndex)), cursor.getString(imgIndex));
cursor.moveToNext();
}
}
}

View File

@@ -1,160 +0,0 @@
package com.example.android.popularmovies;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.support.annotation.NonNull;
import android.util.Log;
import android.widget.ImageView;
import com.nytegear.android.network.NetworkUtil;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
/**
* Created by Aaron Helton on 1/30/2016
*/
public class Movie implements Comparable<Movie>
{
private static final String LOG_TAG = Movie.class.getSimpleName();
public static final String API_KEY = BuildConfig.TMDBKey;
//public static final String API_KEY = "6de0f0590c832161c5da34a0b67f23d5";
public static final String TMDB_URL_BASE = "http://api.themoviedb.org/3";
public static final String TMDB_POSTER_URL = "http://image.tmdb.org/t/p/w185";
public static Movie getMovie(Integer id, Context context)
{
String url = TMDB_URL_BASE+"/movie/"+id +"?api_key="+API_KEY;
String movieJSON = NetworkUtil.getURL(url);
if(movieJSON == null || movieJSON.isEmpty())
return null;
try {
JSONObject movieObject = new JSONObject(movieJSON);
JSONArray genreList = movieObject.getJSONArray("genres");
String[] genres = new String[genreList.length()];
for(int i = 0; i < genreList.length(); i++)
genres[i] = genreList.getJSONObject(i).getString("name");
String title = movieObject.getString("original_title");
String overview = movieObject.getString("overview");
String posterPath = movieObject.getString("poster_path");
String releaseDate = movieObject.getString("release_date");
//Individual Tries for Malformed Movies
int runTime = 0;
int votes = 0;
double vote = 0;
try {
runTime = movieObject.getInt("runtime");
} catch (JSONException ex) { Log.e(LOG_TAG, "BROKEN JSON: " + ex.getMessage(), ex); }
try {
votes = movieObject.getInt("vote_count");
} catch (JSONException ex) { Log.e(LOG_TAG, "BROKEN JSON: " + ex.getMessage(), ex); }
try {
vote = movieObject.getDouble("vote_average");
} catch (JSONException ex) { Log.e(LOG_TAG, "BROKEN JSON: " + ex.getMessage(), ex); }
Bitmap poster = null;
try {
poster = getPoster(context, posterPath);
} catch (IOException ex) {
Log.e(LOG_TAG, "Unable To Get Movie Poster: " + ex.getMessage(), ex);
}
return new Movie(title, overview, poster, releaseDate, runTime, vote, votes, genres);
} catch (JSONException ex) {
Log.e(LOG_TAG, "ERROR PARSING MOVIE: " + ex.getMessage(), ex);
Log.e(LOG_TAG, "JSON TEXT:\n"+movieJSON);
return null;
}
}
public static Bitmap getPoster(Context context, String posterPath) throws IOException
{
if(context == null || posterPath == null || posterPath.isEmpty())
return null;
return NetworkUtil.getImage(TMDB_POSTER_URL + posterPath, context,
BitmapFactory.decodeResource(context.getResources(), R.drawable.loading));
}
private String title;
private String overview;
private Bitmap poster;
private String releaseDate;
private Integer runTime;
private Integer vote_count;
private Double rating;
private String[] genres;
public Movie(String title, String overview, Bitmap poster, String releaseDate,
Integer runTime, Double rating, Integer vote_count, String[] genreList)
{
this.title = title;
this.overview = overview;
this.poster = poster;
this.releaseDate = releaseDate;
this.runTime = runTime;
this.rating = rating;
this.vote_count = vote_count;
this.genres = genreList;
}
@Override
public int compareTo(@NonNull Movie other)
{
if(this.rating < other.rating) {
return 1;
}
else if(this.rating > other.rating) {
return -1;
}
else {
if(this.vote_count < other.vote_count) {
return 1;
} else if (this.vote_count > other.vote_count) {
return -1;
} else {
return 0;
}
}
}
public void applyPoster(ImageView view)
{
view.setImageBitmap(poster);
}
public String getTitle() {
return title;
}
public String getOverview() {
return overview;
}
public Bitmap getPoster() {
return poster;
}
public String getReleaseDate() {
return releaseDate;
}
public Integer getRunTime() {
return runTime;
}
public Double getRating() {
return rating;
}
public Integer getVoteCount() { return vote_count; }
public String[] getGenres() {
return genres;
}
}

View File

@@ -0,0 +1,181 @@
package com.example.android.popularmovies;
import android.graphics.Bitmap;
import android.support.annotation.NonNull;
import android.widget.ImageView;
/**
* Created by Aaron Helton on 1/30/2016
*/
public class MovieInfo implements Comparable<MovieInfo>
{
@SuppressWarnings("unused")
private static final String LOG_TAG = MovieInfo.class.getSimpleName();
public long id;
public String title;
public String overview;
public Bitmap poster;
public String releaseDate;
public String posterPath;
public int runTime;
public int vote_count;
public double rating;
public double popularity;
public String[] genres;
private MovieInfo()
{
}
public static MovieBuilder buildMovie() {
return new MovieBuilder();
}
public static final class MovieBuilder {
MovieInfo movie;
public MovieBuilder() {
movie = new MovieInfo();
}
public MovieInfo build() {
if(isValid())
return movie;
else {
throw new MalformedMovieException("MovieInfo must have an ID and a Title!");
}
}
public boolean isValid() {
return movie.id >= 0 && !(movie.title == null || movie.title.isEmpty());
}
public MovieBuilder withId(Long id) {
movie.id = id;
return this;
}
public MovieBuilder withTitle(String title) {
movie.title = title;
return this;
}
public MovieBuilder withRuntime(int runTime) {
movie.runTime = runTime;
return this;
}
public MovieBuilder withOverview(String overview) {
movie.overview = overview;
return this;
}
public MovieBuilder withPoster(Bitmap poster) {
movie.poster = poster;
return this;
}
public MovieBuilder withReleaseDate(String date)
{
movie.releaseDate = date;
return this;
}
public MovieBuilder withVoteCount(int votes)
{
movie.vote_count = votes;
return this;
}
public MovieBuilder withRating(double rating)
{
movie.rating = rating;
return this;
}
public MovieBuilder withGenres(String[] genres)
{
movie.genres = genres;
return this;
}
public MovieBuilder withPopularity(double popularity)
{
movie.popularity = popularity;
return this;
}
public MovieBuilder withPosterPath(String path)
{
movie.posterPath = path;
return this;
}
}
@Override
public int compareTo(@NonNull MovieInfo other)
{
if(this.rating < other.rating) {
return 1;
}
else if(this.rating > other.rating) {
return -1;
}
else {
if(this.vote_count < other.vote_count) {
return 1;
} else if (this.vote_count > other.vote_count) {
return -1;
} else {
return 0;
}
}
}
public void applyPoster(ImageView view)
{
view.setImageBitmap(poster);
}
public Long getId() { return id; }
public String getTitle() {
return title;
}
public String getOverview() {
return overview;
}
public Bitmap getPoster() {
return poster;
}
public String getPosterPath() { return posterPath; }
public String getReleaseDate() {
return releaseDate;
}
public Integer getRunTime() {
return runTime;
}
public Double getPopularity() { return popularity; }
public Double getRating() {
return rating;
}
public Integer getVoteCount() { return vote_count; }
public String[] getGenres() {
return genres;
}
public static class MalformedMovieException extends RuntimeException {
public MalformedMovieException(String message) {
super(message);
}
}
}