org.yamj.core.database.dao.ApiDao.java Source code

Java tutorial

Introduction

Here is the source code for org.yamj.core.database.dao.ApiDao.java

Source

/*
 *      Copyright (c) 2004-2013 YAMJ Members
 *      https://github.com/organizations/YAMJ/teams
 *
 *      This file is part of the Yet Another Media Jukebox (YAMJ).
 *
 *      YAMJ 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
 *      any later version.
 *
 *      YAMJ 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 YAMJ.  If not, see <http://www.gnu.org/licenses/>.
 *
 *      Web: https://github.com/YAMJ/yamj-v3
 *
 */
package org.yamj.core.database.dao;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.type.DateType;
import org.hibernate.type.IntegerType;
import org.hibernate.type.LongType;
import org.hibernate.type.StringType;
import org.hibernate.type.TimestampType;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.yamj.core.api.model.CountTimestamp;
import org.yamj.core.api.model.dto.ApiVideoDTO;
import org.yamj.common.type.MetaDataType;
import org.yamj.core.api.model.CountGeneric;
import org.yamj.core.api.model.DataItem;
import org.yamj.core.api.model.DataItemTools;
import org.yamj.core.api.wrapper.ApiWrapperList;
import org.yamj.core.api.model.SqlScalars;
import org.yamj.core.api.model.dto.AbstractApiIdentifiableDTO;
import org.yamj.core.api.model.dto.ApiArtworkDTO;
import org.yamj.core.api.model.dto.ApiSeriesInfoDTO;
import org.yamj.core.api.model.dto.ApiEpisodeDTO;
import org.yamj.core.api.model.dto.ApiGenreDTO;
import org.yamj.core.api.model.dto.ApiPersonDTO;
import org.yamj.core.api.model.dto.ApiSeasonInfoDTO;
import org.yamj.core.api.options.OptionsEpisode;
import org.yamj.core.api.options.OptionsIdArtwork;
import org.yamj.core.api.options.OptionsIndexArtwork;
import org.yamj.core.api.options.OptionsIndexPerson;
import org.yamj.core.api.options.OptionsIndexVideo;
import org.yamj.core.api.wrapper.ApiWrapperSingle;
import org.yamj.core.database.model.type.ArtworkType;
import org.yamj.core.hibernate.HibernateDao;

@Service("apiDao")
public class ApiDao extends HibernateDao {

    private static final Logger LOG = LoggerFactory.getLogger(ApiDao.class);
    private static final String ID = "id";
    private static final String YEAR = "year";
    private static final String GENRE = "genre";
    private static final String TITLE = "title";
    private static final String EPISODE = "episode";
    private static final String SEASON = "season";
    private static final String SEASON_ID = "seasonId";
    private static final String SERIES_ID = "seriesId";
    private static final String FIRST_AIRED = "firstAired";
    private static final String VIDEO_YEAR = "videoYear";
    private static final String ORIGINAL_TITLE = "originalTitle";
    private static final String CACHE_FILENAME = "cacheFilename";
    private static final String CACHE_DIR = "cacheDir";
    // SQL
    private static final String SQL_UNION_ALL = " UNION ALL ";
    private static final String SQL_AS_VIDEO_TYPE_STRING = "' AS videoTypeString";
    private static final String SQL_WHERE_1_EQ_1 = " WHERE 1=1";
    private static final String SQL_COMMA_SPACE_QUOTE = ", '";
    private static final String SQL_ARTWORK_TYPE_IN_ARTWORKLIST = " AND a.artwork_type IN (:artworklist)";
    private static final String SQL_LEFT_JOIN_ARTWORK_GENERATED = " LEFT JOIN artwork_generated ag ON al.id=ag.located_id";
    private static final String SQL_LEFT_JOIN_ARTWORK_LOCATED = " LEFT JOIN artwork_located al ON a.id=al.artwork_id";

    /**
     * Generate the query and load the results into the wrapper
     *
     * @param wrapper
     */
    public void getVideoList(ApiWrapperList<ApiVideoDTO> wrapper) {
        SqlScalars sqlScalars = new SqlScalars(generateSqlForVideoList(wrapper));
        OptionsIndexVideo options = (OptionsIndexVideo) wrapper.getOptions();

        sqlScalars.addScalar(ID, LongType.INSTANCE);
        sqlScalars.addScalar("videoTypeString", StringType.INSTANCE);
        sqlScalars.addScalar(TITLE, StringType.INSTANCE);
        sqlScalars.addScalar(ORIGINAL_TITLE, StringType.INSTANCE);
        sqlScalars.addScalar(VIDEO_YEAR, IntegerType.INSTANCE);
        sqlScalars.addScalar(FIRST_AIRED, StringType.INSTANCE);
        sqlScalars.addScalar(SERIES_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON, LongType.INSTANCE);
        DataItemTools.addDataItemScalars(sqlScalars, options.splitDataitems());

        List<ApiVideoDTO> queryResults = executeQueryWithTransform(ApiVideoDTO.class, sqlScalars, wrapper);
        wrapper.setResults(queryResults);

        if (CollectionUtils.isNotEmpty(queryResults)) {
            if (CollectionUtils.isNotEmpty(options.getArtworkTypes())) {
                // Create and populate the ID list
                Map<MetaDataType, List<Long>> ids = new EnumMap<MetaDataType, List<Long>>(MetaDataType.class);
                for (MetaDataType mdt : MetaDataType.values()) {
                    ids.put(mdt, new ArrayList());
                }

                Map<String, ApiVideoDTO> results = new HashMap<String, ApiVideoDTO>();

                for (ApiVideoDTO single : queryResults) {
                    // Add the item to the map for further processing
                    results.put(ApiArtworkDTO.makeKey(single), single);
                    // Add the ID to the list
                    ids.get(single.getVideoType()).add(single.getId());
                }

                boolean foundArtworkIds = Boolean.FALSE; // Check to see that we have artwork to find
                // Remove any blank entries
                for (MetaDataType mdt : MetaDataType.values()) {
                    if (CollectionUtils.isEmpty(ids.get(mdt))) {
                        ids.remove(mdt);
                    } else {
                        // We've found an artwork, so we can continue
                        foundArtworkIds = Boolean.TRUE;
                    }
                }

                if (foundArtworkIds) {
                    LOG.debug("Found artwork to process, IDs: {}", ids);
                    addArtworks(ids, results, options);
                } else {
                    LOG.debug("No artwork found to process, skipping.");
                }
            } else {
                LOG.debug("Artwork not required, skipping.");
            }
        } else {
            LOG.debug("No results found to process.");
        }
    }

    /**
     * Generate the SQL for the video list
     *
     * Note: In this method MetaDataType.UNKNOWN will return all types
     *
     * @param wrapper
     * @return
     */
    private String generateSqlForVideoList(ApiWrapperList<ApiVideoDTO> wrapper) {
        OptionsIndexVideo options = (OptionsIndexVideo) wrapper.getOptions();
        Map<String, String> includes = options.splitIncludes();
        Map<String, String> excludes = options.splitExcludes();
        List<DataItem> dataItems = options.splitDataitems();

        List<MetaDataType> mdt = options.splitTypes();
        LOG.debug("Getting video list for types: {}", mdt.toString());
        if (CollectionUtils.isNotEmpty(dataItems)) {
            LOG.debug("Additional data items requested: {}", dataItems.toString());
        }

        boolean hasMovie = mdt.contains(MetaDataType.MOVIE);
        boolean hasSeries = mdt.contains(MetaDataType.SERIES);
        boolean hasSeason = mdt.contains(MetaDataType.SEASON);
        boolean hasEpisode = mdt.contains(MetaDataType.EPISODE);

        StringBuilder sbSQL = new StringBuilder();

        // Add the movie entries
        if (hasMovie) {
            sbSQL.append(generateSqlForVideo(true, options, includes, excludes, dataItems));
        }

        if (hasMovie && hasSeries) {
            sbSQL.append(SQL_UNION_ALL);
        }

        // Add the TV series entires
        if (hasSeries) {
            sbSQL.append(generateSqlForSeries(options, includes, excludes, dataItems));
        }

        if ((hasMovie || hasSeries) && hasSeason) {
            sbSQL.append(SQL_UNION_ALL);
        }

        // Add the TV season entires
        if (hasSeason) {
            sbSQL.append(generateSqlForSeason(options, includes, excludes, dataItems));
        }

        if ((hasMovie || hasSeries || hasSeason) && hasEpisode) {
            sbSQL.append(SQL_UNION_ALL);
        }

        // Add the TV episode entries
        if (hasEpisode) {
            sbSQL.append(generateSqlForVideo(false, options, includes, excludes, dataItems));
        }

        // Add the sort string, this will be empty if there is no sort required
        sbSQL.append(options.getSortString());

        LOG.trace("SqlForVideoList: {}", sbSQL);
        return sbSQL.toString();
    }

    /**
     * Create the SQL fragment for the selection of movies
     *
     * @param options
     * @param includes
     * @param excludes
     * @return
     */
    private String generateSqlForVideo(boolean isMovie, OptionsIndexVideo options, Map<String, String> includes,
            Map<String, String> excludes, List<DataItem> dataItems) {
        StringBuilder sbSQL = new StringBuilder();

        sbSQL.append("SELECT vd.id");
        if (isMovie) {
            sbSQL.append(SQL_COMMA_SPACE_QUOTE).append(MetaDataType.MOVIE).append(SQL_AS_VIDEO_TYPE_STRING);
        } else {
            sbSQL.append(SQL_COMMA_SPACE_QUOTE).append(MetaDataType.EPISODE).append(SQL_AS_VIDEO_TYPE_STRING);
        }
        sbSQL.append(", vd.title");
        sbSQL.append(", vd.title_original AS originalTitle");
        sbSQL.append(", vd.publication_year AS videoYear");
        sbSQL.append(", '-1' AS firstAired");
        sbSQL.append(", '-1' AS seriesId");
        sbSQL.append(", vd.season_id AS seasonId");
        sbSQL.append(", '-1' AS season");
        sbSQL.append(", vd.episode AS episode");
        sbSQL.append(DataItemTools.addSqlDataItems(dataItems, "vd"));
        sbSQL.append(" FROM videodata vd");
        // Add genre tables for include and exclude
        if (includes.containsKey(GENRE) || excludes.containsKey(GENRE)) {
            sbSQL.append(", videodata_genres vg, genre g");
        }

        if (isMovie) {
            sbSQL.append(" WHERE vd.episode < 0");
        } else {
            sbSQL.append(" WHERE vd.episode > -1");
        }
        if (options.getId() > 0L) {
            sbSQL.append(" AND vd.id=").append(options.getId());
        }
        // Add joins for genres
        if (includes.containsKey(GENRE) || excludes.containsKey(GENRE)) {
            sbSQL.append(" AND vd.id=vg.data_id");
            sbSQL.append(" AND vg.genre_id=g.id");
            sbSQL.append(" AND g.name='");
            if (includes.containsKey(GENRE)) {
                sbSQL.append(includes.get(GENRE));
            } else {
                sbSQL.append(excludes.get(GENRE));
            }
            sbSQL.append("'");
        }

        if (includes.containsKey(YEAR)) {
            sbSQL.append(" AND vd.publication_year=").append(includes.get(YEAR));
        }

        if (excludes.containsKey(YEAR)) {
            sbSQL.append(" AND vd.publication_year!=").append(includes.get(YEAR));
        }

        // Add the search string, this will be empty if there is no search required
        sbSQL.append(options.getSearchString(false));

        LOG.debug("SQL: {}", sbSQL);
        return sbSQL.toString();
    }

    /**
     * Create the SQL fragment for the selection of series
     *
     * @param options
     * @param includes
     * @param excludes
     * @return
     */
    private String generateSqlForSeries(OptionsIndexVideo options, Map<String, String> includes,
            Map<String, String> excludes, List<DataItem> dataItems) {
        StringBuilder sbSQL = new StringBuilder();

        sbSQL.append("SELECT ser.id");
        sbSQL.append(SQL_COMMA_SPACE_QUOTE).append(MetaDataType.SERIES).append(SQL_AS_VIDEO_TYPE_STRING);
        sbSQL.append(", ser.title");
        sbSQL.append(", ser.title_original AS originalTitle");
        sbSQL.append(", ser.start_year AS videoYear");
        sbSQL.append(", '-1' AS firstAired");
        sbSQL.append(", ser.id AS seriesId");
        sbSQL.append(", '-1' AS seasonId");
        sbSQL.append(", '-1' AS season");
        sbSQL.append(", '-1' AS episode");
        sbSQL.append(DataItemTools.addSqlDataItems(dataItems, "ser"));
        sbSQL.append(" FROM series ser ");
        sbSQL.append(SQL_WHERE_1_EQ_1); // To make it easier to add the optional include and excludes
        if (options.getId() > 0L) {
            sbSQL.append(" AND ser.id=").append(options.getId());
        }

        if (includes.containsKey(YEAR)) {
            sbSQL.append(" AND ser.start_year=").append(includes.get(YEAR));
        }

        if (excludes.containsKey(YEAR)) {
            sbSQL.append(" AND ser.start_year!=").append(includes.get(YEAR));
        }

        // Add the search string, this will be empty if there is no search required
        sbSQL.append(options.getSearchString(false));

        return sbSQL.toString();
    }

    /**
     * Create the SQL fragment for the selection of seasons
     *
     * @param options
     * @param includes
     * @param excludes
     * @return
     */
    private String generateSqlForSeason(OptionsIndexVideo options, Map<String, String> includes,
            Map<String, String> excludes, List<DataItem> dataItems) {
        StringBuilder sbSQL = new StringBuilder();

        sbSQL.append("SELECT sea.id");
        sbSQL.append(SQL_COMMA_SPACE_QUOTE).append(MetaDataType.SEASON).append(SQL_AS_VIDEO_TYPE_STRING);
        sbSQL.append(", sea.title");
        sbSQL.append(", sea.title_original AS originalTitle");
        sbSQL.append(", -1 as videoYear");
        sbSQL.append(", sea.first_aired AS firstAired");
        sbSQL.append(", sea.series_id AS seriesId");
        sbSQL.append(", sea.id AS seasonId");
        sbSQL.append(", sea.season AS season");
        sbSQL.append(", '-1' AS episode");
        sbSQL.append(DataItemTools.addSqlDataItems(dataItems, "sea"));
        sbSQL.append(" FROM season sea");
        sbSQL.append(SQL_WHERE_1_EQ_1); // To make it easier to add the optional include and excludes
        if (options.getId() > 0L) {
            sbSQL.append(" AND sea.id=").append(options.getId());
        }

        if (includes.containsKey(YEAR)) {
            sbSQL.append(" AND sea.first_aired LIKE '").append(includes.get(YEAR)).append("%'");
        }

        if (excludes.containsKey(YEAR)) {
            sbSQL.append(" AND sea.first_aired NOT LIKE '").append(includes.get(YEAR)).append("%'");
        }

        // Add the search string, this will be empty if there is no search required
        sbSQL.append(options.getSearchString(false));

        return sbSQL.toString();
    }

    /**
     * Search the list of IDs for artwork and add to the artworkList.
     *
     * @param ids
     * @param artworkList
     * @param options
     */
    private void addArtworks(Map<MetaDataType, List<Long>> ids, Map<String, ApiVideoDTO> artworkList,
            OptionsIndexVideo options) {
        List<String> artworkRequired = options.getArtworkTypes();
        LOG.debug("Artwork required: {}", artworkRequired.toString());

        if (CollectionUtils.isNotEmpty(artworkRequired)) {
            SqlScalars sqlScalars = new SqlScalars();
            boolean hasMovie = CollectionUtils.isNotEmpty(ids.get(MetaDataType.MOVIE));
            boolean hasSeries = CollectionUtils.isNotEmpty(ids.get(MetaDataType.SERIES));
            boolean hasSeason = CollectionUtils.isNotEmpty(ids.get(MetaDataType.SEASON));
            boolean hasEpisode = CollectionUtils.isNotEmpty(ids.get(MetaDataType.EPISODE));

            if (hasMovie) {
                sqlScalars.addToSql(
                        "SELECT 'MOVIE' as sourceString, v.id as sourceId, a.id as artworkId, al.id as locatedId, ag.id as generatedId, a.artwork_type as artworkTypeString, ag.cache_dir as cacheDir, ag.cache_filename as cacheFilename");
                sqlScalars.addToSql(" FROM videodata v, artwork a");
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_LOCATED);
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_GENERATED);
                sqlScalars.addToSql(" WHERE v.id=a.videodata_id");
                sqlScalars.addToSql(" AND v.episode<0");
                sqlScalars.addToSql(" AND v.id IN (:movielist)");
                sqlScalars.addToSql(SQL_ARTWORK_TYPE_IN_ARTWORKLIST);
            }

            if (hasMovie && hasSeries) {
                sqlScalars.addToSql(" UNION");
            }

            if (hasSeries) {
                sqlScalars.addToSql(
                        " SELECT 'SERIES' as sourceString, s.id as sourceId, a.id as artworkId, al.id as locatedId, ag.id as generatedId, a.artwork_type as artworkTypeString, ag.cache_dir as cacheDir, ag.cache_filename as cacheFilename");
                sqlScalars.addToSql(" FROM series s, artwork a");
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_LOCATED);
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_GENERATED);
                sqlScalars.addToSql(" WHERE s.id=a.series_id");
                sqlScalars.addToSql(" AND s.id IN (:serieslist)");
                sqlScalars.addToSql(SQL_ARTWORK_TYPE_IN_ARTWORKLIST);
            }

            if ((hasMovie || hasSeries) && hasSeason) {
                sqlScalars.addToSql(" UNION");
            }

            if (hasSeason) {
                sqlScalars.addToSql(
                        " SELECT 'SEASON' as sourceString, s.id as sourceId, a.id as artworkId, al.id as locatedId, ag.id as generatedId, a.artwork_type as artworkTypeString, ag.cache_dir as cacheDir, ag.cache_filename as cacheFilename");
                sqlScalars.addToSql(" FROM season s, artwork a");
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_LOCATED);
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_GENERATED);
                sqlScalars.addToSql(" WHERE s.id=a.season_id");
                sqlScalars.addToSql(" AND s.id IN (:seasonlist)");
                sqlScalars.addToSql(SQL_ARTWORK_TYPE_IN_ARTWORKLIST);
            }

            if ((hasMovie || hasSeries || hasSeason) && hasEpisode) {
                sqlScalars.addToSql(" UNION");
            }

            if (hasEpisode) {
                sqlScalars.addToSql(
                        "SELECT 'EPISODE' as sourceString, v.id as sourceId, a.id as artworkId, al.id as locatedId, ag.id as generatedId, a.artwork_type as artworkTypeString, ag.cache_dir as cacheDir, ag.cache_filename as cacheFilename");
                sqlScalars.addToSql(" FROM videodata v, artwork a");
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_LOCATED);
                sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_GENERATED);
                sqlScalars.addToSql(" WHERE v.id=a.videodata_id");
                sqlScalars.addToSql(" AND v.episode>-1");
                sqlScalars.addToSql(" AND v.id IN (:episodelist)");
                sqlScalars.addToSql(SQL_ARTWORK_TYPE_IN_ARTWORKLIST);
            }

            sqlScalars.addScalar("sourceString", StringType.INSTANCE);
            sqlScalars.addScalar("sourceId", LongType.INSTANCE);
            sqlScalars.addScalar("artworkId", LongType.INSTANCE);
            sqlScalars.addScalar("locatedId", LongType.INSTANCE);
            sqlScalars.addScalar("generatedId", LongType.INSTANCE);
            sqlScalars.addScalar("artworkTypeString", StringType.INSTANCE);
            sqlScalars.addScalar(CACHE_DIR, StringType.INSTANCE);
            sqlScalars.addScalar(CACHE_FILENAME, StringType.INSTANCE);

            if (hasMovie) {
                sqlScalars.addParameters("movielist", ids.get(MetaDataType.MOVIE));
            }

            if (hasSeries) {
                sqlScalars.addParameters("serieslist", ids.get(MetaDataType.SERIES));
            }

            if (hasSeason) {
                sqlScalars.addParameters("seasonlist", ids.get(MetaDataType.SEASON));
            }

            if (hasEpisode) {
                sqlScalars.addParameters("episodelist", ids.get(MetaDataType.EPISODE));
            }

            sqlScalars.addParameters("artworklist", artworkRequired);

            List<ApiArtworkDTO> results = executeQueryWithTransform(ApiArtworkDTO.class, sqlScalars, null);

            LOG.trace("Found {} artworks", results.size());
            for (ApiArtworkDTO ia : results) {
                LOG.trace("  {} = {}", ia.Key(), ia.toString());
                artworkList.get(ia.Key()).addArtwork(ia);
            }
        }
    }

    /**
     * Get a single Count and Timestamp
     *
     * @param type
     * @param tablename
     * @param clause
     * @return
     */
    public CountTimestamp getCountTimestamp(MetaDataType type, String tablename, String clause) {
        if (StringUtils.isBlank(tablename)) {
            return null;
        }

        StringBuilder sql = new StringBuilder("SELECT '").append(type).append("' as typeString, ");
        sql.append("count(*) as count, ");
        sql.append("MAX(create_timestamp) as createTimestamp, ");
        sql.append("MAX(update_timestamp) as updateTimestamp, ");
        sql.append("MAX(id) as lastId ");
        sql.append("FROM ").append(tablename);
        if (StringUtils.isNotBlank(clause)) {
            sql.append(" WHERE ").append(clause);
        }

        SqlScalars sqlScalars = new SqlScalars(sql);

        sqlScalars.addScalar("typeString", StringType.INSTANCE);
        sqlScalars.addScalar("count", LongType.INSTANCE);
        sqlScalars.addScalar("createTimestamp", TimestampType.INSTANCE);
        sqlScalars.addScalar("updateTimestamp", TimestampType.INSTANCE);
        sqlScalars.addScalar("lastId", LongType.INSTANCE);

        List<CountTimestamp> results = executeQueryWithTransform(CountTimestamp.class, sqlScalars, null);
        if (CollectionUtils.isEmpty(results)) {
            return new CountTimestamp(type);
        }

        return results.get(0);
    }

    /**
     * Get a list of the people
     *
     * @param wrapper
     */
    public void getPersonList(ApiWrapperList<ApiPersonDTO> wrapper) {
        OptionsIndexPerson options = (OptionsIndexPerson) wrapper.getOptions();
        SqlScalars sqlScalars = generateSqlForPerson(options);
        List<ApiPersonDTO> results = executeQueryWithTransform(ApiPersonDTO.class, sqlScalars, wrapper);
        if (CollectionUtils.isNotEmpty(results)) {
            if (options.hasDataItem(DataItem.ARTWORK)) {
                LOG.info("Adding photos");
                // Get the artwork associated with the IDs in the results
                Map<Long, List<ApiArtworkDTO>> artworkList = getArtworkForId(MetaDataType.PERSON,
                        generateIdList(results), Arrays.asList("PHOTO"));
                for (ApiPersonDTO p : results) {
                    if (artworkList.containsKey(p.getId())) {
                        p.setArtwork(artworkList.get(p.getId()));
                    }
                }
            }
        }

        wrapper.setResults(results);
    }

    /**
     * Get a single person using the ID in the wrapper options.
     *
     * @param wrapper
     */
    public void getPerson(ApiWrapperSingle<ApiPersonDTO> wrapper) {
        OptionsIndexPerson options = (OptionsIndexPerson) wrapper.getOptions();
        SqlScalars sqlScalars = generateSqlForPerson(options);
        List<ApiPersonDTO> results = executeQueryWithTransform(ApiPersonDTO.class, sqlScalars, wrapper);
        if (CollectionUtils.isNotEmpty(results)) {
            ApiPersonDTO person = results.get(0);
            if (options.hasDataItem(DataItem.ARTWORK)) {
                LOG.info("Adding photo for {}", person.getName());
                // Add the artwork
                Map<Long, List<ApiArtworkDTO>> artworkList = getArtworkForId(MetaDataType.PERSON, person.getId(),
                        Arrays.asList("PHOTO"));
                if (artworkList.containsKey(options.getId())) {
                    LOG.info("Found {} artworks", artworkList.get(options.getId()).size());
                    person.setArtwork(artworkList.get(options.getId()));
                } else {
                    LOG.info("No artwork found for Person ID '{}'", options.getId());
                }
            }

            if (options.hasDataItem(DataItem.FILMOGRAPHY)) {
                LOG.info("Adding filmograpgy for {}", person.getName());
                LOG.warn("Not implemented yet!");
            }

            wrapper.setResult(person);
        } else {
            wrapper.setResult(null);
        }
    }

    private void getFilmographyForPerson(Object id) {
        SqlScalars sqlScalars = new SqlScalars();

        sqlScalars.addToSql("SELECT " + MetaDataType.MOVIE + " AS videoType,");
        sqlScalars.addToSql("c.job, c.role, c.videodata_id AS videoId,");
        sqlScalars.addToSql("v.title AS videoTitle, v.publication_year as videoYear,");
        sqlScalars.addToSql("-1 AS seasonId, -1 AS season, -1 AS episode");
        sqlScalars.addToSql("FROM person p, cast_crew c, videodata v");
        sqlScalars.addToSql("WHERE p.id=c.person_id");
        sqlScalars.addToSql("AND c.videodata_id = v.id");
        sqlScalars.addToSql("AND v.episode<0");
        sqlScalars.addToSql("AND p.id IN (:id)");
        sqlScalars.addToSql("");
        sqlScalars.addToSql("");

        /*
         Select p.id, p.name, "MOVIE" as type,
         c.job, c.role, c.videodata_id as data_id,
         v.title as title, v.publication_year as year, -1 as season, -1 as episode
         from person p, cast_crew c, videodata v
         where p.id=c.person_id
         and c.videodata_id = v.id
         and p.id=2
         and v.episode<0
         UNION ALL
         Select p.id, p.name, "SEASON" as type,
         c.job, c.role, s.id as data_id,
         v.title_original as title, LEFT(s.first_aired,4) as year, s.season, v.episode
         from person p, cast_crew c, videodata v, season s
         where p.id=c.person_id
         and c.videodata_id = v.id
         and v.season_id=s.id
         and p.id=2
         and v.episode>=0
         ORDER by type, data_id, season, episode
         */
    }

    public void getPersonListByVideoType(MetaDataType metaDataType, ApiWrapperList<ApiPersonDTO> wrapper) {
        OptionsIndexPerson options = (OptionsIndexPerson) wrapper.getOptions();
        LOG.info("Getting person list for {} with ID '{}'", metaDataType, options.getId());

        SqlScalars sqlScalars = generateSqlForVideoPerson(metaDataType, options);
        List<ApiPersonDTO> results = executeQueryWithTransform(ApiPersonDTO.class, sqlScalars, wrapper);
        LOG.info("Found {} results for {} with id '{}'", results.size(), metaDataType, options.getId());

        if (options.hasDataItem(DataItem.ARTWORK) && results.size() > 0) {
            LOG.info("Looking for person artwork for {} with id '{}'", metaDataType, options.getId());

            Map<Long, List<ApiArtworkDTO>> artworkList = getArtworkForId(MetaDataType.PERSON,
                    generateIdList(results), Arrays.asList("PHOTO"));
            for (ApiPersonDTO person : results) {
                if (artworkList.containsKey(person.getId())) {
                    person.setArtwork(artworkList.get(person.getId()));
                }
            }
        } else {
            LOG.info("No artwork found/requested for {} with id '{}'", metaDataType, options.getId());
        }

        wrapper.setResults(results);
    }

    /**
     * Generates a list of people in a video
     *
     * @param metaDataType
     * @param options
     * @return
     */
    private SqlScalars generateSqlForVideoPerson(MetaDataType metaDataType, OptionsIndexPerson options) {
        SqlScalars sqlScalars = new SqlScalars();

        sqlScalars.addToSql("SELECT DISTINCT p.id,");
        sqlScalars.addToSql(" p.name,");
        if (options.hasDataItem(DataItem.BIOGRAPHY)) {
            sqlScalars.addToSql(" p.biography,");
            sqlScalars.addScalar("biography", StringType.INSTANCE);
        }
        sqlScalars.addToSql(" p.birth_day AS birthDay,");
        sqlScalars.addToSql(" p.birth_place AS birthPlace,");
        sqlScalars.addToSql(" p.birth_name AS birthName,");
        sqlScalars.addToSql(" p.death_day AS deathDay,");
        sqlScalars.addToSql(" c.job,");
        sqlScalars.addToSql(" c.role");
        sqlScalars.addToSql(" FROM person p, cast_crew c");
        sqlScalars.addToSql(" WHERE p.id=c.person_id");

        // TODO: Split by series/season/episode
        if (metaDataType == MetaDataType.MOVIE) {
            sqlScalars.addToSql(" AND c.videodata_id=:id");
        } else if (metaDataType == MetaDataType.SERIES) {
            sqlScalars.addToSql("AND c.videodata_id IN");
            sqlScalars.addToSql(" (SELECT DISTINCT v.id FROM season s, videodata v");
            sqlScalars.addToSql(" WHERE s.series_id = :id AND s.id = v.season_id)");
        } else if (metaDataType == MetaDataType.SEASON) {
            sqlScalars.addToSql("AND c.videodata_id IN");
            sqlScalars.addToSql(" (SELECT DISTINCT v.id FROM season s, videodata v");
            sqlScalars.addToSql(" WHERE s.id = :id AND s.id = v.season_id)");
        } else if (metaDataType == MetaDataType.EPISODE) {
            sqlScalars.addToSql(" AND c.videodata_id=:id");
        } else {
            throw new UnsupportedOperationException(
                    "Person list by '" + metaDataType.toString() + "' not supported.");
        }

        if (CollectionUtils.isNotEmpty(options.getJob())) {
            sqlScalars.addToSql(" AND c.job IN (:joblist)");
            sqlScalars.addParameters("joblist", options.getJob());
        }

        // Add the search string
        sqlScalars.addToSql(options.getSearchString(Boolean.FALSE));
        // This will default to blank if there's no sort required
        sqlScalars.addToSql(options.getSortString());

        // Add the ID
        sqlScalars.addParameters(ID, options.getId());

        sqlScalars.addScalar(ID, LongType.INSTANCE);
        sqlScalars.addScalar("name", StringType.INSTANCE);
        sqlScalars.addScalar("birthDay", DateType.INSTANCE);
        sqlScalars.addScalar("birthPlace", StringType.INSTANCE);
        sqlScalars.addScalar("birthName", StringType.INSTANCE);
        sqlScalars.addScalar("deathDay", DateType.INSTANCE);
        sqlScalars.addScalar("job", StringType.INSTANCE);
        sqlScalars.addScalar("role", StringType.INSTANCE);

        LOG.debug("SQL ForVideoPerson: {}", sqlScalars.getSql());
        return sqlScalars;
    }

    /**
     * Generate the SQL for the information about a person
     *
     * @param options
     * @return
     */
    private SqlScalars generateSqlForPerson(OptionsIndexPerson options) {
        SqlScalars sqlScalars = new SqlScalars();
        List<DataItem> dataitems = options.splitDataitems();
        // Make sure to set the alias for the files for the Transformation into the class
        sqlScalars.addToSql("SELECT DISTINCT p.id,");
        sqlScalars.addToSql(" p.name,");
        if (dataitems.contains(DataItem.BIOGRAPHY)) {
            sqlScalars.addToSql(" p.biography, ");
            sqlScalars.addScalar("biography", StringType.INSTANCE);
        }
        sqlScalars.addToSql(" p.birth_day AS birthDay, ");
        sqlScalars.addToSql(" p.birth_place AS birthPlace, ");
        sqlScalars.addToSql(" p.birth_name AS birthName, ");
        sqlScalars.addToSql(" p.death_day AS deathDay ");
        sqlScalars.addToSql(" FROM person p");
        if (CollectionUtils.isNotEmpty(options.getJob())) {
            sqlScalars.addToSql(", cast_crew c");
        }
        if (options.getId() > 0L) {
            sqlScalars.addToSql(" WHERE id=:id");
            sqlScalars.addParameters(ID, options.getId());
        } else {
            sqlScalars.addToSql(SQL_WHERE_1_EQ_1);
        }
        if (CollectionUtils.isNotEmpty(options.getJob())) {
            sqlScalars.addToSql(" AND p.id=c.person_id");
            sqlScalars.addToSql(" AND c.job IN (:joblist)");
            sqlScalars.addParameters("joblist", options.getJob());
        }
        // Add the search string
        sqlScalars.addToSql(options.getSearchString(Boolean.FALSE));
        // This will default to blank if there's no sort required
        sqlScalars.addToSql(options.getSortString());

        sqlScalars.addScalar(ID, LongType.INSTANCE);
        sqlScalars.addScalar("name", StringType.INSTANCE);
        sqlScalars.addScalar("birthDay", DateType.INSTANCE);
        sqlScalars.addScalar("birthPlace", StringType.INSTANCE);
        sqlScalars.addScalar("birthName", StringType.INSTANCE);
        sqlScalars.addScalar("deathDay", DateType.INSTANCE);

        return sqlScalars;
    }

    //<editor-fold defaultstate="collapsed" desc="Artwork Methods">
    public ApiArtworkDTO getArtworkById(Long id) {
        SqlScalars sqlScalars = getSqlArtwork(new OptionsIndexArtwork(id));

        List<ApiArtworkDTO> results = executeQueryWithTransform(ApiArtworkDTO.class, sqlScalars, null);
        if (CollectionUtils.isEmpty(results)) {
            return new ApiArtworkDTO();
        }

        return results.get(0);
    }

    public List<ApiArtworkDTO> getArtworkList(ApiWrapperList<ApiArtworkDTO> wrapper) {
        SqlScalars sqlScalars = getSqlArtwork((OptionsIndexArtwork) wrapper.getOptions());
        return executeQueryWithTransform(ApiArtworkDTO.class, sqlScalars, wrapper);
    }

    private SqlScalars getSqlArtwork(OptionsIndexArtwork options) {
        SqlScalars sqlScalars = new SqlScalars();

        sqlScalars.addToSql("SELECT a.id AS artworkId,");
        sqlScalars.addToSql(" al.id AS locatedId,");
        sqlScalars.addToSql(" ag.id AS generatedId,");
        sqlScalars.addToSql(" a.season_id AS seasonId,");
        sqlScalars.addToSql(" a.series_id AS seriesId,");
        sqlScalars.addToSql(" a.videodata_id AS videodataId,");
        sqlScalars.addToSql(" a.artwork_type AS artworkTypeString,");
        sqlScalars.addToSql(" ag.cache_filename AS cacheFilename,");
        sqlScalars.addToSql(" ag.cache_dir AS cacheDir");
        sqlScalars.addToSql(" FROM artwork a");
        sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_LOCATED);
        sqlScalars.addToSql(SQL_LEFT_JOIN_ARTWORK_GENERATED);
        sqlScalars.addToSql(SQL_WHERE_1_EQ_1); // Make appending restrictions easier
        if (options != null) {
            if (options.getId() > 0L) {
                sqlScalars.addToSql(" AND a.id=:id");
                sqlScalars.addParameters(ID, options.getId());
            }

            if (CollectionUtils.isNotEmpty(options.getArtwork())) {
                sqlScalars.addToSql(SQL_ARTWORK_TYPE_IN_ARTWORKLIST);
                sqlScalars.addParameters("artworklist", options.getArtwork());
            }

            if (CollectionUtils.isNotEmpty(options.getVideo())) {
                StringBuilder sb = new StringBuilder("AND (");
                boolean first = Boolean.TRUE;
                for (String type : options.getVideo()) {
                    MetaDataType mdt = MetaDataType.fromString(type);
                    if (first) {
                        first = Boolean.FALSE;
                    } else {
                        sb.append(" OR");
                    }
                    if (mdt == MetaDataType.MOVIE) {
                        sb.append(" videodata_id IS NOT NULL");
                    } else if (mdt == MetaDataType.SERIES) {
                        sb.append(" series_id IS NOT NULL");
                    } else if (mdt == MetaDataType.SEASON) {
                        sb.append(" season_id IS NOT NULL");
                    }
                }
                sb.append(")");
                sqlScalars.addToSql(sb.toString());
            }
        }

        // Add the scalars
        sqlScalars.addScalar("artworkId", LongType.INSTANCE);
        sqlScalars.addScalar("locatedId", LongType.INSTANCE);
        sqlScalars.addScalar("generatedId", LongType.INSTANCE);
        sqlScalars.addScalar(SEASON_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SERIES_ID, LongType.INSTANCE);
        sqlScalars.addScalar("videodataId", LongType.INSTANCE);
        sqlScalars.addScalar("artworkTypeString", StringType.INSTANCE);
        sqlScalars.addScalar(CACHE_DIR, StringType.INSTANCE);
        sqlScalars.addScalar(CACHE_FILENAME, StringType.INSTANCE);

        return sqlScalars;
    }
    //</editor-fold>

    public void getEpisodeList(ApiWrapperList<ApiEpisodeDTO> wrapper) {
        OptionsEpisode options = (OptionsEpisode) wrapper.getOptions();
        SqlScalars sqlScalars = new SqlScalars();

        sqlScalars.addToSql(
                "SELECT ser.id AS seriesId, sea.id AS seasonId, sea.season, vid.episode, vid.id as episodeId, vid.title,");
        if (options.hasDataItem(DataItem.OUTLINE)) {
            sqlScalars.addToSql("vid.outline,");
            sqlScalars.addScalar("outline", StringType.INSTANCE);
        }
        if (options.hasDataItem(DataItem.PLOT)) {
            sqlScalars.addToSql("vid.plot,");
            sqlScalars.addScalar("plot", StringType.INSTANCE);
        }
        sqlScalars.addToSql("ag.cache_filename AS cacheFilename, ag.cache_dir AS cacheDir");
        sqlScalars.addToSql("FROM season sea, series ser, videodata vid, artwork a");
        sqlScalars.addToSql("LEFT JOIN artwork_located al ON a.id=al.artwork_id");
        sqlScalars.addToSql("LEFT JOIN artwork_generated ag ON al.id=ag.located_id");
        sqlScalars.addToSql("WHERE sea.series_id=ser.id");
        sqlScalars.addToSql("AND vid.season_id=sea.id");
        sqlScalars.addToSql("AND a.videodata_id=vid.id");
        if (options.getSeriesid() > 0L) {
            sqlScalars.addToSql("AND ser.id=:seriesid");
            sqlScalars.addParameters("seriesid", options.getSeriesid());
            if (options.getSeason() > 0L) {
                sqlScalars.addToSql("AND sea.season=:season");
                sqlScalars.addParameters(SEASON, options.getSeason());
            }
        }
        if (options.getSeasonid() > 0L) {
            sqlScalars.addToSql("AND sea.id=:seasonid");
            sqlScalars.addParameters("seasonid", options.getSeasonid());
        }
        sqlScalars.addToSql("ORDER BY seriesId, season, episode");

        sqlScalars.addScalar(SERIES_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON, LongType.INSTANCE);
        sqlScalars.addScalar(EPISODE, LongType.INSTANCE);
        sqlScalars.addScalar("episodeId", LongType.INSTANCE);
        sqlScalars.addScalar(TITLE, StringType.INSTANCE);
        sqlScalars.addScalar(CACHE_FILENAME, StringType.INSTANCE);
        sqlScalars.addScalar(CACHE_DIR, StringType.INSTANCE);

        List<ApiEpisodeDTO> results = executeQueryWithTransform(ApiEpisodeDTO.class, sqlScalars, wrapper);
        wrapper.setResults(results);
    }

    public void getSingleVideo(ApiWrapperSingle<ApiVideoDTO> wrapper) {
        OptionsIndexVideo options = (OptionsIndexVideo) wrapper.getOptions();
        Map<String, String> includes = options.splitIncludes();
        Map<String, String> excludes = options.splitExcludes();
        MetaDataType type = MetaDataType.fromString(options.getType());

        List<DataItem> dataItems = options.splitDataitems();
        LOG.debug("Getting additional data items: {} ", dataItems.toString());

        String sql;
        if (type == MetaDataType.MOVIE) {
            sql = generateSqlForVideo(true, options, includes, excludes, dataItems);
        } else if (type == MetaDataType.SERIES) {
            sql = generateSqlForSeries(options, includes, excludes, dataItems);
        } else if (type == MetaDataType.SEASON) {
            sql = generateSqlForSeason(options, includes, excludes, dataItems);
        } else {
            throw new UnsupportedOperationException(
                    "Unable to process type '" + type + "' (Original: '" + options.getType() + "')");
        }
        LOG.debug("SQL for {}-{}: {}", type, options.getId(), sql);

        SqlScalars sqlScalars = new SqlScalars(sql);

        sqlScalars.addScalar(ID, LongType.INSTANCE);
        sqlScalars.addScalar("videoTypeString", StringType.INSTANCE);
        sqlScalars.addScalar(TITLE, StringType.INSTANCE);
        sqlScalars.addScalar(ORIGINAL_TITLE, StringType.INSTANCE);
        sqlScalars.addScalar(VIDEO_YEAR, IntegerType.INSTANCE);
        sqlScalars.addScalar(FIRST_AIRED, StringType.INSTANCE);
        sqlScalars.addScalar(SERIES_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON, LongType.INSTANCE);
        // Add Scalars for additional data item columns
        DataItemTools.addDataItemScalars(sqlScalars, dataItems);

        List<ApiVideoDTO> queryResults = executeQueryWithTransform(ApiVideoDTO.class, sqlScalars, wrapper);
        LOG.debug("Found {} results for ID '{}'", queryResults.size(), options.getId());
        if (CollectionUtils.isNotEmpty(queryResults)) {
            ApiVideoDTO video = queryResults.get(0);
            if (dataItems.contains(DataItem.GENRE)) {
                LOG.debug("Adding genres");
                video.setGenres(getGenresForId(MetaDataType.MOVIE, options.getId()));
            }

            if (dataItems.contains(DataItem.ARTWORK)) {
                LOG.debug("Adding artwork");
                Map<Long, List<ApiArtworkDTO>> artworkList;
                if (CollectionUtils.isNotEmpty(options.getArtworkTypes())) {
                    artworkList = getArtworkForId(type, options.getId(), options.getArtworkTypes());
                } else {
                    artworkList = getArtworkForId(type, options.getId());
                }

                if (artworkList.containsKey(options.getId())) {
                    video.setArtwork(artworkList.get(options.getId()));
                }
            }
            wrapper.setResult(video);
        } else {
            wrapper.setResult(null);
        }
    }

    /**
     * Get a list of the Genres for a give video ID
     *
     * @param type
     * @param id
     * @return
     */
    public List<ApiGenreDTO> getGenresForId(MetaDataType type, Long id) {
        SqlScalars sqlScalars = new SqlScalars();
        sqlScalars.addToSql("SELECT g.id, g.name");
        sqlScalars.addToSql("FROM videodata_genres vg, genre g");
        if (type == MetaDataType.SERIES) {
            sqlScalars.addToSql(", series v");
        } else if (type == MetaDataType.SEASON) {
            sqlScalars.addToSql(", season v");
        } else {
            // Default to Movie
            sqlScalars.addToSql(", videodata v");
        }
        sqlScalars.addToSql("WHERE vg.genre_id=g.id");
        sqlScalars.addToSql("AND vg.data_id=:id");
        sqlScalars.addToSql("AND v.id=vg.data_id");

        sqlScalars.addScalar(ID, LongType.INSTANCE);
        sqlScalars.addScalar("name", StringType.INSTANCE);

        sqlScalars.addParameters(ID, id);

        return executeQueryWithTransform(ApiGenreDTO.class, sqlScalars, null);

    }

    /**
     * Get a list of all artwork available for a video ID
     *
     * @param type
     * @param id
     * @return
     */
    public Map<Long, List<ApiArtworkDTO>> getArtworkForId(MetaDataType type, Long id) {
        List<String> artworkRequired = new ArrayList<String>();
        for (ArtworkType at : ArtworkType.values()) {
            artworkRequired.add(at.toString());
        }
        // Remove the unknown type
        artworkRequired.remove(ArtworkType.UNKNOWN.toString());

        return getArtworkForId(type, id, artworkRequired);
    }

    /**
     * Get a select list of artwork available for a video ID
     *
     * @param type
     * @param id
     * @param artworkRequired
     * @return
     */
    public Map<Long, List<ApiArtworkDTO>> getArtworkForId(MetaDataType type, Object id,
            List<String> artworkRequired) {
        LOG.debug("Artwork required for {} ID '{}' is {}", type, id, artworkRequired);
        StringBuilder sbSQL = new StringBuilder();
        sbSQL.append("SELECT '").append(type.toString()).append("' AS sourceString,");
        sbSQL.append(" v.id AS sourceId, a.id AS artworkId, al.id AS locatedId, ag.id AS generatedId,");
        sbSQL.append(
                " a.artwork_type AS artworkTypeString, ag.cache_dir AS cacheDir, ag.cache_filename AS cacheFilename ");
        if (type == MetaDataType.MOVIE) {
            sbSQL.append("FROM videodata v ");
        } else if (type == MetaDataType.SERIES) {
            sbSQL.append("FROM series v ");
        } else if (type == MetaDataType.SEASON) {
            sbSQL.append("FROM season v ");
        } else if (type == MetaDataType.PERSON) {
            sbSQL.append("FROM person v");
        }
        sbSQL.append(", artwork a"); // Artwork must be last for the LEFT JOIN
        sbSQL.append(SQL_LEFT_JOIN_ARTWORK_LOCATED);
        sbSQL.append(SQL_LEFT_JOIN_ARTWORK_GENERATED);
        if (type == MetaDataType.MOVIE) {
            sbSQL.append(" WHERE v.id=a.videodata_id");
            sbSQL.append(" AND v.episode<0");
        } else if (type == MetaDataType.SERIES) {
            sbSQL.append(" WHERE v.id=a.series_id");
        } else if (type == MetaDataType.SEASON) {
            sbSQL.append(" WHERE v.id=a.season_id");
        } else if (type == MetaDataType.PERSON) {
            sbSQL.append(" WHERE v.id=a.person_id");
        }
        sbSQL.append(" AND v.id IN (:id)");
        sbSQL.append(SQL_ARTWORK_TYPE_IN_ARTWORKLIST);

        SqlScalars sqlScalars = new SqlScalars(sbSQL);
        LOG.info("Artwork SQL: {}", sqlScalars.getSql());

        sqlScalars.addScalar("sourceString", StringType.INSTANCE);
        sqlScalars.addScalar("sourceId", LongType.INSTANCE);
        sqlScalars.addScalar("artworkId", LongType.INSTANCE);
        sqlScalars.addScalar("locatedId", LongType.INSTANCE);
        sqlScalars.addScalar("generatedId", LongType.INSTANCE);
        sqlScalars.addScalar("artworkTypeString", StringType.INSTANCE);
        sqlScalars.addScalar(CACHE_DIR, StringType.INSTANCE);
        sqlScalars.addScalar(CACHE_FILENAME, StringType.INSTANCE);

        sqlScalars.addParameters(ID, id);
        sqlScalars.addParameters("artworklist", artworkRequired);

        List<ApiArtworkDTO> results = executeQueryWithTransform(ApiArtworkDTO.class, sqlScalars, null);

        Map<Long, List<ApiArtworkDTO>> artworkList = generateIdMapList(results);

        return artworkList;
    }

    public List<CountGeneric> getJobCount(List<String> requiredJobs) {
        LOG.info("getJobCount: Required Jobs: {}", (requiredJobs == null ? "all" : requiredJobs));
        SqlScalars sqlScalars = new SqlScalars();

        sqlScalars.addToSql("SELECT job AS item, COUNT(*) AS count");
        sqlScalars.addToSql("FROM  cast_crew");
        if (CollectionUtils.isNotEmpty(requiredJobs)) {
            sqlScalars.addToSql("WHERE job IN (:joblist)");
            sqlScalars.addParameters("joblist", requiredJobs);
        }
        sqlScalars.addToSql("GROUP BY job");

        sqlScalars.addScalar("item", StringType.INSTANCE);
        sqlScalars.addScalar("count", LongType.INSTANCE);

        return executeQueryWithTransform(CountGeneric.class, sqlScalars, null);
    }

    public void getSeriesInfo(ApiWrapperList<ApiSeriesInfoDTO> wrapper) {
        OptionsIdArtwork options = (OptionsIdArtwork) wrapper.getOptions();
        Long id = options.getId();
        LOG.info("Getting series information for seriesId '{}'", id);

        SqlScalars sqlScalars = new SqlScalars();
        sqlScalars.addToSql("SELECT s.id AS seriesId, title, start_year AS seriesYear");
        sqlScalars.addToSql("FROM series s");
        sqlScalars.addToSql("WHERE id=:id");
        sqlScalars.addToSql("ORDER BY id");
        sqlScalars.addParameters(ID, options.getId());

        sqlScalars.addScalar(SERIES_ID, LongType.INSTANCE);
        sqlScalars.addScalar(TITLE, StringType.INSTANCE);
        sqlScalars.addScalar("seriesYear", IntegerType.INSTANCE);

        List<ApiSeriesInfoDTO> seriesResults = executeQueryWithTransform(ApiSeriesInfoDTO.class, sqlScalars,
                wrapper);
        LOG.debug("Found {} series for SeriesId '{}'", seriesResults.size(), id);

        for (ApiSeriesInfoDTO series : seriesResults) {
            if (options.hasDataItem(DataItem.ARTWORK)) {
                Map<Long, List<ApiArtworkDTO>> artworkList = getArtworkForId(MetaDataType.SERIES, id,
                        options.getArtworkTypes());
                for (ApiArtworkDTO artwork : artworkList.get(id)) {
                    series.addArtwork(artwork);
                }
            }
            series.setSeasonList(getSeasonInfo(options));
        }
        wrapper.setResults(seriesResults);
    }

    private List<ApiSeasonInfoDTO> getSeasonInfo(OptionsIdArtwork options) {
        Long seriesId = options.getId();

        LOG.debug("Getting season information for seriesId '{}'", seriesId);
        SqlScalars sqlScalars = new SqlScalars();
        sqlScalars.addToSql("SELECT s.series_id AS seriesId, s.id AS seasonId, s.season, title");
        sqlScalars.addToSql("FROM season s");
        sqlScalars.addToSql("WHERE series_id=:id");
        sqlScalars.addToSql("ORDER BY series_id, season");
        sqlScalars.addParameters(ID, seriesId);

        sqlScalars.addScalar(SERIES_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON_ID, LongType.INSTANCE);
        sqlScalars.addScalar(SEASON, IntegerType.INSTANCE);
        sqlScalars.addScalar(TITLE, StringType.INSTANCE);

        List<ApiSeasonInfoDTO> seasonResults = executeQueryWithTransform(ApiSeasonInfoDTO.class, sqlScalars, null);
        LOG.debug("Found {} seasons for SeriesId '{}'", seasonResults.size(), seriesId);

        if (options.hasDataItem(DataItem.ARTWORK)) {
            for (ApiSeasonInfoDTO season : seasonResults) {
                Map<Long, List<ApiArtworkDTO>> artworkList = getArtworkForId(MetaDataType.SEASON,
                        season.getSeasonId(), options.getArtworkTypes());
                if (artworkList == null || !artworkList.containsKey(seriesId)
                        || CollectionUtils.isEmpty(artworkList.get(seriesId))) {
                    LOG.debug("No artwork found for seriesId '{}' season {}", seriesId, season.getSeason());
                } else {
                    for (ApiArtworkDTO artwork : artworkList.get(season.getSeasonId())) {
                        season.addArtwork(artwork);
                    }
                }
            }
        }

        return seasonResults;
    }

    //<editor-fold defaultstate="collapsed" desc="Utility Functions">
    /**
     * Takes a list and generates a map of the ID and item
     *
     * @param idList
     * @return
     */
    private <T extends AbstractApiIdentifiableDTO> Map<Long, T> generateIdMap(List<T> idList) {
        Map<Long, T> results = new HashMap<Long, T>(idList.size());

        for (T idSingle : idList) {
            results.put(idSingle.getId(), idSingle);
        }

        return results;
    }

    /**
     * Take a list and generate a map of the ID and a list of the items for that ID
     *
     * @param <T> Source type
     * @param idList List of the source type
     * @return
     */
    private <T extends AbstractApiIdentifiableDTO> Map<Long, List<T>> generateIdMapList(List<T> idList) {
        Map<Long, List<T>> results = new HashMap<Long, List<T>>();

        for (T idSingle : idList) {
            Long sourceId = idSingle.getId();
            if (results.containsKey(sourceId)) {
                results.get(sourceId).add(idSingle);
            } else {
                // ID didn't exist so add a new list
                results.put(sourceId, new ArrayList<T>(Arrays.asList(idSingle)));
            }
        }

        return results;
    }

    /**
     * Generate a list of the IDs from a list
     *
     * @param idList
     * @return
     */
    private <T extends AbstractApiIdentifiableDTO> List<Long> generateIdList(List<T> idList) {
        List<Long> results = new ArrayList<Long>(idList.size());

        for (T idSingle : idList) {
            results.add(idSingle.getId());
        }

        return results;
    }
    //</editor-fold>
}