/*
* Copyright (C) 2005-2009 Team XBMC
* http://xbmc.org
*
* 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 2, 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 XBMC Remote; see the file license. If not, write to
* the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
* http://www.gnu.org/copyleft/gpl.html
*
*/
package org.xbmc.httpapi.client;
import java.util.ArrayList;
import java.util.HashMap;
import org.xbmc.api.business.INotifiableManager;
import org.xbmc.api.data.IVideoClient;
import org.xbmc.api.data.IControlClient.ICurrentlyPlaying;
import org.xbmc.api.info.PlayStatus;
import org.xbmc.api.object.Actor;
import org.xbmc.api.object.Genre;
import org.xbmc.api.object.Host;
import org.xbmc.api.object.ICoverArt;
import org.xbmc.api.object.Movie;
import org.xbmc.api.type.MediaType;
import org.xbmc.api.type.SortType;
import org.xbmc.httpapi.Connection;
import android.graphics.Bitmap;
/**
* Takes care of everything related to the video database.
*
* @author Team XBMC
*/
public class VideoClient extends Client implements IVideoClient {
private static final String MOVIE_COLUMNS = "idMovie, c00, c07, strPath, strFileName, c15, c11, c14, ROUND(c05, 2), playCount, c09, art.url";
private static final String SELECT_MOVIES = "SELECT" + " " + MOVIE_COLUMNS;
private static final String WHERE_MOVIES = " " + "FROM" + " " + "movie, files, path, art";
public static final String TAG = "VideoClient";
public static final String PLAYLIST_ID = "1";
public static final int PLAYLIST_LIMIT = 100;
/**
* Class constructor needs reference to HTTP client connection
* @param connection
*/
public VideoClient(Connection connection) {
super(connection);
}
/**
* Updates host info on the connection.
* @param host
*/
public void setHost(Host host) {
mConnection.setHost(host);
}
/**
* Gets all movies from database
* @param sortBy Sort field, see SortType.*
* @param sortOrder Sort order, must be either SortType.ASC or SortType.DESC.
* @return All movies
*/
public ArrayList<Movie> getMovies(INotifiableManager manager, int sortBy, String sortOrder, boolean hideWatched) {
StringBuilder sb = new StringBuilder();
sb.append(SELECT_MOVIES);
sb.append(WHERE_MOVIES);
sb.append(" WHERE movie.idFile=files.idFile AND path.idPath=files.idPath AND movie.idMovie=art.media_id AND art.media_type='movie' AND art.type='thumb'");
sb.append(watchedFilter(hideWatched));
sb.append(moviesOrderBy(sortBy, sortOrder));
return parseMovies(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets movies from database with offset
* @param sortBy Sort field, see SortType.*
* @param sortOrder Sort order, must be either SortType.ASC or SortType.DESC.
* @return Movies with offset
*/
public ArrayList<Movie> getMovies(INotifiableManager manager, int sortBy, String sortOrder, int offset, boolean hideWatched) {
StringBuilder sb = new StringBuilder();
sb.append(SELECT_MOVIES);
sb.append(WHERE_MOVIES);
sb.append(" WHERE movie.idFile=files.idFile AND path.idPath=files.idPath AND movie.idMovie=art.media_id AND art.media_type='movie' AND art.type='thumb'");
sb.append(watchedFilter(hideWatched));
sb.append(moviesOrderBy(sortBy, sortOrder));
sb.append(" LIMIT -1 OFFSET " + offset);
return parseMovies(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all movies with an actor from database
* @param actor Display only movies with this actor.
* @param sortBy Sort field, see SortType.*
* @param sortOrder Sort order, must be either SortType.ASC or SortType.DESC.
* @return All movies with an actor
*/
public ArrayList<Movie> getMovies(INotifiableManager manager, Actor actor, int sortBy, String sortOrder, boolean hideWatched) {
StringBuilder sb = new StringBuilder();
sb.append(SELECT_MOVIES);
sb.append(WHERE_MOVIES);
sb.append(" WHERE movie.idFile=files.idFile AND path.idPath=files.idPath AND movie.idMovie=art.media_id AND art.media_type='movie' AND art.type='thumb' AND movie.idmovie IN (");
sb.append(" SELECT DISTINCT idMovie ");
sb.append(" FROM actorlinkmovie ");
sb.append(" WHERE idActor =");
sb.append(actor.id);
sb.append(" )");
sb.append(watchedFilter(hideWatched));
sb.append(moviesOrderBy(sortBy, sortOrder));
return parseMovies(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all movies of a genre from database
* @param genre Display only movies of this genre.
* @param sortBy Sort field, see SortType.*
* @param sortOrder Sort order, must be either SortType.ASC or SortType.DESC.
* @return All movies of a genre
*/
public ArrayList<Movie> getMovies(INotifiableManager manager, Genre genre, int sortBy, String sortOrder, boolean hideWatched) {
StringBuilder sb = new StringBuilder();
sb.append(SELECT_MOVIES);
sb.append(WHERE_MOVIES);
sb.append(" WHERE movie.idFile=files.idFile AND path.idPath=files.idPath AND movie.idMovie=art.media_id AND art.media_type='movie' AND art.type='thumb' AND movie.idmovie IN (");
sb.append(" SELECT DISTINCT idMovie ");
sb.append(" FROM genrelinkmovie ");
sb.append(" WHERE idGenre =");
sb.append(genre.id);
sb.append(" )");
sb.append(watchedFilter(hideWatched));
sb.append(moviesOrderBy(sortBy, sortOrder));
return parseMovies(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all movies from database
* @param sortBy Sort field, see SortType.*
* @param sortOrder Sort order, must be either SortType.ASC or SortType.DESC.
* @return Updated movie
*/
public Movie updateMovieDetails(INotifiableManager manager, Movie movie) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT c03, c01, c04, c18, c12, c19");
sb.append(WHERE_MOVIES);
sb.append(" WHERE movie.idFile=files.idFile AND path.idPath=files.idPath AND movie.idMovie=art.media_id AND art.media_type='movie' AND art.type='thumb' AND movie.idmovie = ");
sb.append(movie.getId());
parseMovieDetails(mConnection.query("QueryVideoDatabase", sb.toString(), manager), movie);
sb = new StringBuilder();
sb.append("SELECT actors.idActor, strActor, art.url, strRole");
sb.append(" FROM actors LEFT OUTER JOIN art ON art.media_id=idActor AND art.media_type='actor' and art.type='thumb', actorlinkmovie");
sb.append(" WHERE actors.idActor = actorlinkmovie.idActor");
sb.append(" AND actorlinkmovie.idMovie =");
sb.append(movie.getId());
movie.actors = parseActorRoles(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
return movie;
}
/**
* Gets all actors from database. Use {@link getMovieActors()} and
* {@link getTvActors()} for filtered actors.
* @return All actors
*/
public ArrayList<Actor> getActors(INotifiableManager manager) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT idActor, strActor, art.url");
sb.append(" FROM actors LEFT OUTER JOIN art ON art.media_id=idActor AND art.media_type='actor' and art.type='thumb'");
sb.append(" ORDER BY upper(strActor), strActor");
return parseActors(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all movie actors from database
* @return All movie actors
*/
public ArrayList<Actor> getMovieActors(INotifiableManager manager) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT DISTINCT actors.idActor, strActor, art.url");
sb.append(" FROM actors LEFT OUTER JOIN art ON art.media_id=actors.idActor AND art.media_type='actor' and art.type='thumb', actorlinkmovie");
sb.append(" WHERE actorlinkmovie.idActor = actors.idActor");
sb.append(" ORDER BY upper(strActor), strActor");
return parseActors(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all movie actors from database
* @return All movie actors
*/
public ArrayList<Actor> getTvShowActors(INotifiableManager manager) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT DISTINCT actors.idActor, strActor, art.url");
sb.append(" FROM actors LEFT OUTER JOIN art ON art.media_id=actors.idActor AND art.media_type='actor' and art.type='thumb', actorlinktvshow");
sb.append(" WHERE actorlinktvshow.idActor = actors.idActor");
sb.append(" ORDER BY upper(strActor), strActor");
return parseActors(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all movie genres from database
* @return All movie genres
*/
public ArrayList<Genre> getMovieGenres(INotifiableManager manager) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT idGenre, strGenre FROM genre");
sb.append(" WHERE idGenre IN (SELECT idGenre FROM genrelinkmovie)");
sb.append(" ORDER BY upper(strGenre)");
return parseGenres(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Gets all tv show genres from database
* @return All tv show genres
*/
public ArrayList<Genre> getTvShowGenres(INotifiableManager manager) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT idGenre, strGenre FROM genre");
sb.append(" WHERE idGenre IN (SELECT idGenre FROM genrelinktvshow)");
sb.append(" ORDER BY upper(strGenre)");
return parseGenres(mConnection.query("QueryVideoDatabase", sb.toString(), manager));
}
/**
* Returns a pre-resized movie cover. Pre-resizing is done in a way that
* the bitmap at least as large as the specified size but not larger than
* the double.
* @param manager Postback manager
* @param cover Cover object
* @param size Minmal size to pre-resize to.
* @return Thumbnail bitmap
*/
public Bitmap getCover(INotifiableManager manager, ICoverArt cover, int size) {
return getCover(manager, cover, size, Movie.getThumbUri(cover), Movie.getFallbackThumbUri(cover));
}
/**
* Converts query response from HTTP API to a list of Movie objects. Each
* row must return the following attributes in the following order:
* <ol>
* <li><code>idMovie</code></li>
* <li><code>c00</code></li> (title)
* <li><code>c07</code></li> (year)
* <li><code>strPath</code></li>
* <li><code>strFileName</code></li>
* <li><code>c15</code></li> (director)
* <li><code>c11</code></li> (runtime)
* <li><code>c14</code></li> (genres)
* <li><code>c05</code></li> (rating)
* <li><code>playCount</code></li> (numWatched)
* <li><code>c09</code></li> (imdbId)
* <li><code>c08</code></li> (art url for hashing to get filename)
* </ol>
* @param response
* @return List of movies
*/
private ArrayList<Movie> parseMovies(String response) {
ArrayList<Movie> movies = new ArrayList<Movie>();
String[] fields = response.split("<field>");
try {
for (int row = 1; row < fields.length; row += 12) { //WHen adding a field, be sure to change this #
movies.add(new Movie( // int id, String title, int year, String path, String filename, String director, String runtime, String genres, Double rating, int numWatched, String imdbId
Connection.trimInt(fields[row]),
Connection.trim(fields[row + 1]),
Connection.trimInt(fields[row + 2]),
Connection.trim(fields[row + 3]),
Connection.trim(fields[row + 4]),
Connection.trim(fields[row + 5]),
Connection.trim(fields[row + 6]),
Connection.trim(fields[row + 7]),
Connection.trimDouble(fields[row + 8]),
Connection.trimInt(fields[row + 9]),
Connection.trim(fields[row + 10]),
Connection.trim(fields[row + 11])
));
}
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
System.err.println("response = " + response);
e.printStackTrace();
}
return movies;
}
/**
* Updates a movie object with some more details. Fields must be the following (in this order):
* <ol>
* <li><code>c03</code></li> (tagline)
* <li><code>c01</code></li> (plot)
* <li><code>c04</code></li> (number of votes)
* <li><code>c18</code></li> (studio)
* <li><code>c12</code></li> (parental rating)
* <li><code>c19</code></li> (trailer)
* </ol>
* @param response
* @param movie
* @return Updated movie object
*/
private Movie parseMovieDetails(String response, Movie movie) {
String[] fields = response.split("<field>");
try {
movie.tagline = Connection.trim(fields[1]);
movie.plot = Connection.trim(fields[2]);
movie.numVotes = Connection.trimInt(fields[3]);
movie.studio = Connection.trim(fields[4]);
movie.rated = Connection.trim(fields[5]);
movie.trailerUrl = Connection.trim(fields[6]);
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
System.err.println("response = " + response);
e.printStackTrace();
}
return movie;
}
/**
* Converts query response from HTTP API to a list of Actor objects. Each
* row must return the following columns in the following order:
* <ol>
* <li><code>idActor</code></li>
* <li><code>strActor</code></li>
* </ol>
* @param response
* @return List of Actors
*/
public static ArrayList<Actor> parseActors(String response) {
ArrayList<Actor> actors = new ArrayList<Actor>();
String[] fields = response.split("<field>");
try {
for (int row = 1; row < fields.length; row += 3) {
actors.add(new Actor(
Connection.trimInt(fields[row]),
Connection.trim(fields[row + 1]),
Connection.trim(fields[row+2])
));
}
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
System.err.println("response = " + response);
e.printStackTrace();
}
return actors;
}
/**
* Converts query response from HTTP API to a list of Actor objects with
* roles attached. Each row must return the following columns in the
* following order:
* <ol>
* <li><code>idActor</code></li>
* <li><code>strActor</code></li>
* <li><code>strRole</code></li>
* </ol>
* @param response
* @return List of Actors
*/
public static ArrayList<Actor> parseActorRoles(String response) {
ArrayList<Actor> actors = new ArrayList<Actor>();
String[] fields = response.split("<field>");
try {
for (int row = 1; row < fields.length; row += 4) {
actors.add(new Actor(
Connection.trimInt(fields[row]),
Connection.trim(fields[row + 1]),
Connection.trim(fields[row + 2]),
Connection.trim(fields[row + 3])
));
}
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
System.err.println("response = " + response);
e.printStackTrace();
}
return actors;
}
/**
* Converts query response from HTTP API to a list of Genre objects. Each
* row must return the following columns in the following order:
* <ol>
* <li><code>idGenre</code></li>
* <li><code>strGenre</code></li>
* </ol>
* @param response
* @return List of Genres
*/
public static ArrayList<Genre> parseGenres(String response) {
ArrayList<Genre> genres = new ArrayList<Genre>();
String[] fields = response.split("<field>");
try {
for (int row = 1; row < fields.length; row += 2) {
genres.add(new Genre(
Connection.trimInt(fields[row]),
Connection.trim(fields[row + 1])
));
}
} catch (Exception e) {
System.err.println("ERROR: " + e.getMessage());
System.err.println("response = " + response);
e.printStackTrace();
}
return genres;
}
private String watchedFilter(boolean hideWatched) {
if (hideWatched) {
return " AND (playCount IS NULL OR playCount = 0) ";
} else {
return "";
}
}
/**
* Returns an SQL String of given sort options of movies query
* @param sortBy Sort field
* @param sortOrder Sort order
* @return SQL "ORDER BY" string
*/
private String moviesOrderBy(int sortBy, String sortOrder) {
switch (sortBy) {
default:
case SortType.TITLE:
return " ORDER BY CASE WHEN c10 IS NULL OR c10 = '' THEN replace(lower(c00),'the ','') ELSE replace(lower(c10),'the ','') END " + sortOrder;
case SortType.YEAR:
return " ORDER BY c07 " + sortOrder + ", CASE WHEN c10 IS NULL OR c10 = '' THEN lower(c00) ELSE lower(c10) END " + sortOrder;
case SortType.RATING:
return " ORDER BY ROUND(c05, 2) " + sortOrder;
case SortType.DATE_ADDED:
return " ORDER BY files.idFile " + sortOrder;
}
}
static ICurrentlyPlaying getCurrentlyPlaying(final HashMap<String, String> map) {
return new ICurrentlyPlaying() {
private static final long serialVersionUID = 5036994329211476714L;
public String getTitle() {
String title = map.get("Title");
if (title != null)
return title;
String[] path = map.get("Filename").replaceAll("\\\\", "/").split("/");
return path[path.length - 1];
}
public int getTime() {
return parseTime(map.get("Time"));
}
public int getPlayStatus() {
return PlayStatus.parse(map.get("PlayStatus"));
}
public int getPlaylistPosition() {
return Integer.parseInt(map.get("VideoNo"));
}
//Workarond for bug in Float.valueOf(): http://code.google.com/p/android/issues/detail?id=3156
public float getPercentage() {
try{
return Integer.valueOf(map.get("Percentage"));
} catch (NumberFormatException e) { }
return Float.valueOf(map.get("Percentage"));
}
public String getFilename() {
return map.get("Filename");
}
public int getDuration() {
return parseTime(map.get("Duration"));
}
public String getArtist() {
return map.get("Genre");
}
public String getAlbum() {
String title = map.get("Tagline");
if (title != null)
return title;
String path = map.get("Filename").replaceAll("\\\\", "/");
return path.substring(0, path.lastIndexOf("/"));
}
public int getMediaType() {
return MediaType.VIDEO;
}
public boolean isPlaying() {
return PlayStatus.parse(map.get("PlayStatus")) == PlayStatus.PLAYING;
}
public int getHeight() {
return 0;
}
public int getWidth() {
return 0;
}
private int parseTime(String time) {
String[] s = time.split(":");
if (s.length == 2) {
return Integer.parseInt(s[0]) * 60 + Integer.parseInt(s[1]);
} else if (s.length == 3) {
return Integer.parseInt(s[0]) * 3600 + Integer.parseInt(s[1]) * 60 + Integer.parseInt(s[2]);
} else {
return 0;
}
}
};
}
/**
* Retrieves the currently playing video number in the playlist.
* @return Number of items in the playlist
*/
public int getPlaylistPosition(INotifiableManager manager) {
return mConnection.getInt(manager, "GetPlaylistSong");
}
/**
* Sets the media at playlist position to be the next item to be played.
* @param position New position, starting with 0.
* @return True on success, false otherwise.
*/
public boolean setPlaylistPosition(INotifiableManager manager, int position) {
return mConnection.getBoolean(manager, "SetPlaylistSong", String.valueOf(position));
}
/**
* Returns the first {@link PLAYLIST_LIMIT} videos of the playlist.
* @return Videos in the playlist.
*/
public ArrayList<String> getPlaylist(INotifiableManager manager) {
return mConnection.getArray(manager, "GetPlaylistContents", PLAYLIST_ID);
}
/**
* Removes media from the current playlist. It is not possible to remove the media if it is currently being played.
* @param position Complete path (including filename) of the media to be removed.
* @return True on success, false otherwise.
*/
public boolean removeFromPlaylist(INotifiableManager manager, String path) {
return mConnection.getBoolean(manager, "RemoveFromPlaylist", PLAYLIST_ID + ";" + path);
}
}