TVDB::API - API to www.thetvdb.com


TVDB-API documentation Contained in the TVDB-API distribution.

Index


Code Index:

NAME

Top

TVDB::API - API to www.thetvdb.com

SYNOPSIS

Top

  use TVDB::API;

  my $tvdb = TVDB::API::new([[$apikey], $language]);

  $tvdb->setApiKey($apikey);
  $tvdb->setLang('en');
  $tvdb->setUserAgent("TVDB::API/$VERSION");
  $tvdb->setBannerPath("/foo/bar/banners");
  $tvdb->setCacheDB("$ENV{HOME}/.tvdb.db");

  my $hashref = $tvdb->getConf();
  my $value = $tvdb->getConf($key);
  $tvdb->setConf($key, $value);
  $tvdb->setConf({key1=>'value1', key2=>'value2'});

  my $hashref = $tvdb->getAvailableMirrors([$nocache]);
  $tvdb->setMirrors($mirror, [$banner, [$zip]]);
  $tvdb->chooseMirrors([$nocache]);
  $tvdb->getAvailableLanguages([$nocache]);

  $tvdb->getUpdates([$period]);

  my $series_id = $tvdb->getPossibleSeriesId($series_name, [$nocache]);
  my $series_id = $tvdb->getSeriesId($series_name, [$nocache]);
  my $name = $tvdb->getSeriesName($series_id, [$nocache]);
  my $hashref = $tvdb->getSeries($series_name, [$nocache]);
  my $hashref = $tvdb->getSeriesAll($series_name, [$nocache]);
  my $hashref = $tvdb->getSeriesActors($series_name, [$nocache]);
  my $hashref = $tvdb->getSeriesActorsSorted($series_name, [$nocache]);
  my $hashref = $tvdb->getSeriesBanners($series_name, $type, $type2, $value, [$nocache]);
  my $hashref = $tvdb->getSeriesInfo($series_name, key, [$nocache]);
  my $string = $tvdb->getSeriesBanner($series_name, [$buffer, [$nocache]]);
  my $string = $tvdb->getSeriesFanart($series_name, [$buffer, [$nocache]]);
  my $string = $tvdb->getSeriesPoster($series_name, [$buffer, [$nocache]]);
  my $string = $tvdb->getSeriesOverview($series_name, [$nocache]);

  my $path = $tvdb->getBanner($banner, [$buffer, [$nocache]]);

  my $int = $tvdb->getMaxSeason($series, [$nocache]);
  my $hashref = $tvdb->getSeason($series, $season, [$nocache]);
  my @picture_names = $tvdb->getSeasonBanners($series, $season, [$nocache]);
  my $string = $tvdb->getSeasonBanner($series, $season, [$buffer, [$nocache]]);
  my @picture_names = $tvdb->getSeasonBannersWide($series, $season, [$nocache]);
  my $string = $tvdb->getSeasonBannerWide($series, $season, [$buffer, [$nocache]]);

  my $int = $tvdb->getMaxEpisode($series, $season, [$nocache]);
  my $hashref = $tvdb->getEpisode($series, $season, $episode, [$nocache]);
  my $hashref = $tvdb->getEpisodeAbs($series, $absEpisode, [$nocache]);
  my $hashref = $tvdb->getEpisodeDVD($series, $DVDseason, $DVDepisode, [$nocache]);
  my $hashref = $tvdb->getEpisodeId($episodeid, [$nocache]);
  my $hashref = $tvdb->getEpisodeByAirDate($series, $airdate, [$nocache]);
  my $string = $tvdb->getEpisodeInfo($series, $season, $episode, $info, [$nocache]);
  my $string = $tvdb->getEpisodeBanner($series, $season, $episode, [$buffer, [$nocache]]);
  my $string = $tvdb->getEpisodeName($series, $season, $episode, [$nocache]);
  my $string = $tvdb->getEpisodeOverview($series, $season, $episode, [$nocache]);

  my $hashref = $tvdb->getRatingsForUser($userid, $series, [$nocache]);

  $tvdb->dumpCache();

DESCRIPTION

Top

This module provides an API to the TVDB database through the new published API.

$tvdb = TVDB::API::new([APIKEY, [LANGUAGE]])

Create a TVDB::API object using APIKEY and using a default language of LANGUAGE. Both these arguments are optional.

New can also be called with a hashref as the first argument.

  $tvdb = TVDB::API::new({ apikey    => $apikey,
                           lang      => 'en',
                           cache     => 'filename',
                           banner    => 'banner/path',
                           useragent => 'My useragent'
                        });

setApiKey(APIKEY);

Set the APIKEY to be used to access the web api for thetvdb.com

setLang(LANGUAGE);

Set the LANGUAGE to use when downloading data from thetvdb.com

setUserAgent(USERAGENT);

Set the USERAGENT to be used when downloading information from thetvdb.com

setBannerPath(PATH);

Set the path in which to save downloaded banner graphics files.

setCacheDB("$ENV{HOME}/.tvdb.db");

Set the name of the database file to be used to save data from thetvdb.com

getAvailableMirrors([NOCACHE]);

Get the list of mirror sites available from thetvdb.com. It returns a hashref of arrays. If NOCACHE is non-zero, then the mirrors are downloaded again even if they are in the cache database already.

Returns: { xml => @xml_mirrors, banner => @banner_mirrors, zip => @zip_mirrors, }

setMirrors(MIRROR, [BANNER, [ZIP]])

Set the mirror site(s) to be used to download tv info. If BANNER or ZIP or not specified, then MIRROR is used instead.

chooseMirrors([NOCACHE])

Choose a random mirror from the list of available mirrors. If NOCACHE is non-zero, then the mirrors are downloaded again even if they are in the cache database already.

getConf([KEY])

Get configurable values by KEY. If no KEY is specified, a hashref of all values is returned.

setConf(KEY, VALUE) or setConf({KEY=>VALUE, ...})

Set configurable values by KEY/VALUE pair. If a hashref is passed in, all KEY/VALUE pairs in the hashref will be configured.

    maxSeason       => 50,          # Maximum allowed season
    maxEpisode      => 50,          # Maximum allowed episode
    minUpdateTime   => 3600*6,      # Used by getUpdate('now')
    minBannerTime   => 3600*24*7,   # Used by getBanner()
    minEpisodeTime  => 3600*24*7,   # Used by getEpisode()

getAvailableLanguages([NOCACHE])

Get a list of available languages, and return them in a hashref. If NOCACHE is non-zero, then the available languages are downloaded again even if they are in the cache database already.

getUpdates([PERIOD])

Get appropriate updates (day/week/month/all) from thetvdb.com based on the specified PERIOD. It then downloads updates for series, episodes, and banners which have already been downloaded.

day

Get the updates for the last 24 hours (86400 seconds).

week

Get the updates for the last week (7 days, or 604800 seconds).

month

Get the updates for the last month (30 days, or 2592000 seconds).

all

Get all updates available.

now

Based on the last update performed, determine whether to do a day, week, month or all update.

guess

Like now, based on the last update performed; determine whether to do a day, week, month or all update. However, if the last update was performed in the last 6 hours (setable as minUpdateTime with setConf()), do nothing. This is the default PERIOD.

getPossibleSeriesId(SERIESNAME)

Get a list of possible series ids for SERIESNAME from thetvtb.com. This will return a hashref of possibilities.

getSeriesId(SERIESNAME, [NOCACHE])

Get the series id (an integer) for SERIESNAME from thetvtb.com. If NOCACHE is non-zero, then the series id is downloaded again even if it is in the cache database already.

getSeriesName(SERIESID, [NOCACHE])

Get the series name (a string) for SERIESID. If NOCACHE is non-zero, then the series name is downloaded again even if it is in the cache database already.

getSeries(SERIESNAME, [NOCACHE])

Get the series info for SERIESNAME from thetvtb.com, which is returned as a hashref. If NOCACHE is non-zero, then the series info is downloaded again even if it is in the cache database already.

getSeriesAll(SERIESNAME, [NOCACHE])

Get the series info, and all episodes for SERIESNAME from thetvtb.com, which is returned as a hashref. If NOCACHE is non-zero, then the series info and episodes are downloaded again even if they are in the cache database already.

getSeriesActors(SERIESNAME, [NOCACHE])

Get the actors for SERIESNAME from thetvtb.com, which is returned as a hashref. If NOCACHE is non-zero, then the list of actors are downloaded again even if they are in the cache database already.

getSeriesActorsSorted(SERIESNAME, [NOCACHE])

Get the actors for SERIESNAME from thetvtb.com, which is returned as an arrayref sorted by SortOrder. If NOCACHE is non-zero, then the list of actors are downloaded again even if they are in the cache database already.

getSeriesBanners(SERIESNAME, TYPE, TYPE2, VALUE, [NOCACHE])

Get the banners for SERIESNAME from thetvtb.com. Info about the available banners are returned in a hashref. The actual banners can be downloaded individually with getBanner (see below). If NOCACHE is non-zero, then the list of banners are downloaded again even if they are in the cache database already.

if TYPE is specified (series, season, poster, or fanart) then only return banners of that type. if TYPE2 is specified then only return banners of that sub type. If TYPE is "series" then TYPE2 can be "text", "graphical", or "blank". If TYPE is "season" then TYPE2 can be "season", or "seasonwide" and VALUE specifies the season number. If TYPE is "fanart" then TYPE2 is the desired resolution of the image.

getSeriesInfo(SERIESNAME, KEY, [NOCACHE])

Return a string for KEY in the hashref for SERIESNAME. If NOCACHE is non-zero, then the series is downloaded again even if it is in the cache database already.

getSeriesBanner(SERIESNAME, [BUFFER, [NOCACHE]])

Get the SERIESNAME banner from thetvdb.com and save it in the BannerPath directory. The cached banner is updated via getUpdates when appropriate. If a BUFFER is provided (a scalar reference), the banner (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the banner is downloaded again even if it is in the BannerPath directory already. It will return the path of the banner relative to the BannerPath directory.

getSeriesFanart(SERIESNAME, [BUFFER, [NOCACHE]])

Get the SERIESNAME fan art from thetvdb.com and save it in the BannerPath directory. The cached fan art is updated via getUpdates when appropriate. If a BUFFER is provided (a scalar reference), the fan art (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the fan art is downloaded again even if it is in the BannerPath directory already. It will return the path of the fan art relative to the BannerPath directory.

getSeriesPoster(SERIESNAME, [BUFFER, [NOCACHE]])

Get the SERIESNAME poster from thetvdb.com and save it in the BannerPath directory. The cached poster is updated via getUpdates when appropriate. If a BUFFER is provided (a scalar reference), the poster (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the poster is downloaded again even if it is in the BannerPath directory already. It will return the path of the poster relative to the BannerPath directory.

getSeriesOverview(SERIESNAME, [NOCACHE])

Get the series overview from thetvdb.com and return it as a string. If NOCACHE is non-zero, then the banner is downloaded again even if it is in the cache database already.

getBanner(BANNER, [BUFFER, [NOCACHE]])

Get the BANNER from thetvdb.com and save it in the BannerPath directory. The cached banner is updated via getUpdates when appropriate. If a BUFFER is provided (a scalar reference), the picture (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the banner is downloaded again even if it is in the BannerPath directory already. It will return the path of the picture relative to the BannerPath directory. In this case it will just be the same as BANNER.

The minBannerTime configuration variable determines the maximum time a banner download failure will be cached. (see getConf()/setConf()).

getMaxSeason(SERIESNAME, [NOCACHE])

Return the number of the last season for SERIESNAME. If NOCACHE is non-zero, then any series info needed to calculate this is downloaded again even if it is in the cache database already.

getSeason(SERIESNAME, SEASON, [NOCACHE])

Return a hashref of episodes in SEASON for SERIESNAME. If NOCACHE is non-zero, then any episodes needed for this season is downloaded again even if it is in the cache database already.

The maxSeason configuration variable determines the maximum allowable season (see getConf()/setConf()).

getSeasonBanners(SERIESNAME, SEASON, [NOCACHE])

Return an array of banner names for SEASON for SERIESNAME. These names can get used with getBanner() to actually download the banner file. If NOCACHE is non-zero, then any data needed for this is downloaded again even if it is in the cache database already.

getSeasonBanner(SERIESNAME, SEASON, [BUFFER, [NOCACHE]])

Get a random banner for SEASON for SERIESNAME. The cached banner is updated via getUpdates when appropriate. If a BUFFER is provided (a scalar reference), the banner (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the banner is downloaded again even if it is in the BannerPath directory already. It will return the path of the banner relative to the BannerPath directory.

getSeasonBannersWide(SERIESNAME, SEASON, [NOCACHE])

Return an array of wide banner names for SEASON for SERIESNAME. These names can get used with getBanner() to actually download the banner file. If NOCACHE is non-zero, then any data needed for this is downloaded again even if it is in the BannerPath directory already.

getSeasonBannerWide(SERIESNAME, SEASON, [BUFFER, [NOCACHE]])

Get a random banner for SEASON for SERIESNAME. The cached banner is updated via getUpdates when appropriate. If a BUFFER is provided (a scalar reference), the banner (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the banner is downloaded again even if it is in the BannerPath directory already. It will return the path of the banner relative to the BannerPath directory.

getMaxEpisode(SERIESNAME, SEASON, [NOCACHE])

Return the number episodes in SEASON for SERIESNAME. If NOCACHE is non-zero, then any series info needed to calculate this is downloaded again even if it is in the cache database already.

The maxEpisode configuration variable determines the maximum allowable episode (see getConf()/setConf()).

getEpisode(SERIESNAME, SEASON, EPISODE, [NOCACHE])

Return a hashref for the EPISODE in SEASON for SERIESNAME. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

The minEpisodeTime configuration variable determines the maximum time a episode lookup failure will be cached. (see getConf()/setConf()).

getEpisodeAbs(SERIESNAME, ABSEPISODE, [NOCACHE])

Return a hashref for the absolute episode (ABSEPISODE) for SERIESNAME. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

getEpisodeDVD(SERIESNAME, SEASON, EPISODE, [NOCACHE])

Return a hashref for the EPISODE in SEASON for SERIESNAME in DVD order. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

getEpisodeId(EPISODEID, [NOCACHE])

Return a hashref for the episode indicated by EPISODEID. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

getEpisodeByAirDate(SERIESNAME, AIRDATE [NOCACHE])

Return a hashref for the episode in SERIESNAME on AIRDATE. AIRDATE can be specified as:

    2008-01-01
    2008-1-1
    January 1, 2008
    1/1/2008

Currently this lookup is not cached. However, if NOCACHE is non-zero, then the SERIESNAME to seriesid lookup is downloaded again.

getEpisodeInfo(SERIESNAME, SEASON, EPISODE, KEY, [NOCACHE])

Return a string for KEY in the hashref for EPISODE in SEASON for SERIESNAME. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

getEpisodeBanner(SERIESNAME, SEASON, EPISODE, [BUFFER, [NOCACHE]])

Get the episode banner for EPISODE in SEASON for SERIESNAME. The cached banner is updated via getUpdates when appropriate. If a BUFFER is provided, the picture (newly downloaded, or from the cache) is loaded into it. If NOCACHE is non-zero, then the banner is downloaded again even if it is in the BannerPath directory already. It will return the path of the picture relative to the BannerPath directory.

getEpisodeName(SERIESNAME, SEASON, EPISODE, [NOCACHE])

Return the episode name for EPISODE in SEASON for SERIESNAME. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

getEpisodeOverview(SERIESNAME, SEASON, EPISODE, [NOCACHE])

Return the overview for EPISODE in SEASON for SERIESNAME. If NOCACHE is non-zero, then the episode is downloaded again even if it is in the cache database already.

getRatingsForUser(USERID, SERIESNAME, [NOCACHE])

Get the series ratings for USERID. If SERIESNAME is specified, the user/community ratings for the series and its episodes are returned in a hashref. If SERIESNAME is not specified, then all the series rated by the <USERID> will be returned in a hashref. These lookups are not cached.

dumpCache()

Dump the cache database with Dumper to stdout.

EXAMPLE

Top

    use Data::Dumper;
    use TVDB::API;
    my $episode = $tvdb->getEpisode('Lost', 3, 5);
    print Dumper($episode);

    Produces:

    $episode = {
      'lastupdated' => '1219734325',
      'EpisodeName' => 'The Cost of Living',
      'seasonid' => '16270',
      'Overview' => 'A delirious Eko wrestles with past demons; some of the castaways go to the Pearl station to find a computerthey can use to locate Jack, Kate and Sawyer; Jack does not know who to trust when two of the Others are at odds with each other.',
      'filename' => 'episodes/73739-308051.jpg',
      'EpisodeNumber' => '5',
      'Language' => 'en',
      'Combined_season' => '3',
      'FirstAired' => '2006-11-01',
      'seriesid' => '73739',
      'Director' => 'Jack Bender',
      'SeasonNumber' => '3',
      'Writer' => 'Monica Owusu-Breen, Alison Schapker',
      'GuestStars' => '|Olalekan Obileye| Kolawole Obileye Junior| Alicia Young| Aisha Hinds| Lawrence Jones| Ariston Green| Michael Robinson| Jermaine|',
      'Combined_episodenumber' => '5'
    };

AUTHOR

Top

Behan Webster <behanw@websterwood.com>

COPYRIGHT

Top


TVDB-API documentation Contained in the TVDB-API distribution.

# Copyright (c) 2008 Behan Webster. All rights reserved. This program is free
# software; you can redistribute it and/or modify it under the same terms
# as Perl itself.

package TVDB::API;

require 5.008008;
use strict;

use Compress::Zlib;
use DBM::Deep;
use Data::Dumper;
use Debug::Simple;
use Encode qw(encode decode);
use IO::Uncompress::Unzip;
use LWP;
use Storable;
use XML::Simple;

use vars qw($VERSION %Defaults %Url);

$VERSION = "0.33";

# TheTVDB Urls
%Url = (
	defaultURL	=> 'http://thetvdb.com',
	getSeriesID	=> '%s/api/GetSeries.php?seriesname=%s&language=%s',	# defaultURL, series_name, language
	getMirrors	=> '%s/api/%s/mirrors.xml',			# defaultURL, apikey
	bannerURL	=> '%s/banners/',				# baseBannerURL, append bannerFilename.ext
	apiURL		=> '%s/api/%s',					# mirrorURL, apikey
	getLanguages	=> '%s/languages.xml',				# apiURL
	getSeries	=> '%s/series/%s/%s.xml',			# apiURL, seriesid, language
	getSeriesAll	=> '%s/series/%s/all/%s.%s',			# apiURL, seriesid, language, (xml|zip)
	getSeriesActors	=> '%s/series/%s/actors.xml',			# apiURL, seriesid
	getSeriesBanner	=> '%s/series/%s/banners.xml',			# apiURL, seriesid
	getEpisode	=> '%s/series/%s/default/%s/%s/%s.xml',		# apiURL, seriesid, season, episode, language
	getEpisodeDVD	=> '%s/series/%s/dvd/%s/%s/%s.xml',		# apiURL, seriesid, season, episode, language
	getEpisodeAbs	=> '%s/series/%s/absolute/%s/%s.xml',		# apiURL, seriesid, absolute_episode, language
	getEpisodeID	=> '%s/episodes/%s/%s.xml',			# apiURL, episodeid, language
	getUpdates	=> '%s/updates/updates_%s.%s',			# apiURL, (day|week|month|all), (xml|zip)

	getEpisodeByAirDate	=> '%s/api/GetEpisodeByAirDate.php?apikey=%s&seriesid=%s&airdate=%s&language=%s',
	getRatingsForUser	=> '%s/api/GetRatingsForUser.php?apikey=%s&accountid=%s&seriesid=%s',
	getRatingsForUserAll	=> '%s/api/GetRatingsForUser.php?apikey=%s&accountid=%s',
);

%Defaults = (
	maxSeason		=> 50,
	maxEpisode		=> 50,
	minUpdateTime	=> 3600*6,		# 6 hours 
	minBannerTime	=> 3600*24*7,	# 1 week 
	minEpisodeTime	=> 3600*24*7,	# 1 week 
);

###############################################################################
sub new {
	my $self = bless {};
	%{$self->{conf}} = %Defaults;

	my $args;
	if (ref $_[0] eq 'HASH') {
		# Subroutine arguments by hashref
		$args = shift;
	} else {
		# Traditional subroutine arguments
		$args = {};
		($args->{apikey}, $args->{lang}, $args->{cache}, $args->{banner}, @{$args->{mirrors}}) = @_;
	}
	# Argument defaults
	$args->{cache}     ||= "$ENV{HOME}/.tvdb.db";
	$args->{apikey}    ||= die 'You need to get an apikey from http://thetvdb.com/?tab=apiregister';
	$args->{useragent} ||= "TVDB::API/$VERSION";

	$self->setCacheDB($args->{cache});
	$self->setApiKey($args->{apikey});
	$self->{ua} = LWP::UserAgent->new;
	$self->{ua}->env_proxy();
	$self->setUserAgent($args->{useragent});
	$self->{xml} = XML::Simple->new(
		ForceArray => ['Actor', 'Banner', 'Episode', 'Mirror', 'Series'],
		SuppressEmpty => 1,
	);

	if (@{$args->{mirrors}}) {
		$self->setMirrors(@{$args->{mirrors}});
	} else {
		$self->chooseMirrors();
	}

	# The following must be after setCacheDB/setApiKey/setUserAgent/xml/setMirrors
	$self->setLang($args->{lang});
	$self->setBannerPath($args->{banner}) if $args->{banner};

	return $self;
}

###############################################################################
sub setApiKey {
	my ($self, $apikey) = @_;
	$self->{apikey} = $apikey;
	$self->_updateUrls();
}
sub setLang {
	my $self = shift;
	my $lang = shift || 'en';
	my $langs = $self->getAvailableLanguages();
	&verbose(3, "TVDB::API: Setting language to: $lang => $langs->{$lang}->{name}\n");
	$self->{lang} = $lang;
}
sub setMirrors {
	my $self = shift;
	$self->{mirror} = shift || $Url{defaultURL};
	$self->{banner} = shift || $self->{mirror} || '';
	$self->{zip} = shift || $self->{mirror} || '';
	&verbose(3, "TVDB::API: Setting mirrors to: xml:$self->{mirror} banner:$self->{banner} zip:$self->{zip}\n");
	$self->_updateUrls();
}
sub _updateUrls {
	my ($self) = @_;
	$self->{apiURL} = sprintf $Url{apiURL}, $self->{mirror}, $self->{apikey};
	$self->{bannerURL} = sprintf $Url{bannerURL}, $self->{banner};
	$self->{zipURL} = sprintf $Url{apiURL}, $self->{zip}, $self->{apikey};
}
sub setUserAgent {
	my ($self, $userAgent) = @_;
	$self->{ua}->agent($userAgent);
}
sub setBannerPath {
	my ($self, $path) = @_;
	$self->{bannerPath} = $path;
	mkdir $path;
	return -d $path;
}

###############################################################################
sub setCacheDB {
	my ($self, $cache) = @_;
	$self->{cachefile} = $cache;
	$self->{cache} = DBM::Deep->new(
		file => $cache,
		#filter_store_key => \&_compressCache,
		filter_store_value => \&_compressCache,
		#filter_fetch_key => \&_decompressCache,
		filter_fetch_value => \&_decompressCache,
		utf8 => 1,
	);
}
sub _compressCache {
	# Escape UTF-8 chars and gzip data
	return Compress::Zlib::memGzip(encode('utf8',$_[0])) ;
}
sub _decompressCache {
	# Decompress data and then unescape UTF-8 chars
	return decode('utf8',Compress::Zlib::memGunzip($_[0])) ;
}
sub dumpCache {
	my ($self) = @_;
	my $cache = $self->{cache};
	print Dumper($cache);
}

###############################################################################
sub setConf {
	my ($self, $key, $value) = @_;
	if (ref $key eq 'HASH') {
		while (my ($k, $v) = each %$key) {
			$self->{conf}->{$k} = $v;
		}
	} else {
		$self->{conf}->{$key} = $value;
	}
}
sub getConf {
	my ($self, $key) = @_;
	return $self->{conf}->{$key} if $key && defined $self->{conf}->{$key};
	return $self->{conf};
}

###############################################################################
# Download binary data
sub _download {
	my ($self, $fmt, $url, @parm) = @_;

	# Make URL
	$url = sprintf($fmt, $url, @parm);
	&verbose(2, "TVDB::API: download: $url\n");
	utf8::encode($url);

	# Make sure we only download once even in a session
	return $self->{dload}->{$url} if defined $self->{dload}->{$url};

	# Download URL
	my $req = HTTP::Request->new(GET => $url);
	my $res = $self->{ua}->request($req);

	if ($res->content =~ /(?:404 Not Found|The page your? requested does not exist)/i) {
		&warning("TVDB::API: download $url, 404 Not Found\n");
		$self->{dload}->{$url} = 0;
		return undef;
	}
	$self->{dload}->{$url} = $res->content;
	return $res->content;
}
# Download Xml, remove empty tags, parse XML, and return hashref
sub _downloadXml {
	my ($self, $fmt, @parm) = @_;

	# Download XML file
	my $xml = $self->_download($fmt, $self->{apiURL}, @parm, 'xml');
	return undef unless $xml;

	# Remove empty tags
	$xml =~ s/(<[^\/\s>]*\/>|<[^\/\s>]*><\/[^>]*>)//gs;

	# Return process XML into hashref
	return $self->{xml}->XMLin($xml);
}
# Download Xml, remove empty tags, parse XML, and return hashref
sub _downloadApikeyXml {
	my ($self, $fmt, @parm) = @_;

	# Download XML file
	my $xml = $self->_download($fmt, $self->{mirror}, $self->{apikey}, @parm);
	return undef unless $xml;

	$xml =~ s/seriesid>/id>/g;

	# Remove empty tags
	$xml =~ s/(<[^\/\s>]*\/>|<[^\/\s>]*><\/[^>]*>)//gs;

	# Return process XML into hashref
	return $self->{xml}->XMLin($xml);
}
# Download Zip file, decompress into one Xml file, remove empty tags, parse XML, and return hashref
sub _downloadZip {
	my ($self, $fmt, @parm) = @_;

	# Download XML file
	my $zip = $self->_download($fmt, $self->{zipURL}, @parm, 'zip');
	return undef unless $zip;

	# Uncompress ZIP
	my $url = sprintf($fmt, $self->{zipURL}, @parm, 'zip');
	my $obj = new IO::Uncompress::Unzip \$zip, MultiStream => 1, Transparent => 1
		or die "IO::Uncompress::Unzip failed: $url\n";
	local $/ = undef;
	my $xml = <$obj>;

	# Make en.xml/banners.xml/actors.xml into one xml file
	if ($xml =~ s/<\/Data><\?xml.*?Banners>|<\/Banners><\?xml.*?Actors>//gs) {
		$xml =~ s/<\/Actors>$/<\/Data>/s;
	}

	# Remove empty tags
	$xml =~ s/(<[^\/\s>]*\/>|<[^\/\s>]*><\/[^>]*>)//gs;

	&debug(4, "download Zip: $url\n", XML => \$xml);

	# Return process XML into hashref
	return $self->{xml}->XMLin($xml);
}

###############################################################################
sub getAvailableMirrors {
	my ($self, $nocache) = @_;

	my $cache = $self->{cache};
	if ($nocache || not defined $cache->{Mirror}) {
		# Get list of mirrors
		my $xml = $self->_download($Url{getMirrors}, $Url{defaultURL}, $self->{apikey});
		my $data = XMLin($xml, ForceArray=>['Mirror']);

		# Break into lists of mirror types: xml/banner/zip
		$self->{cache}->{Mirror} = {};
		while (my ($key,$value) = each %{$data->{Mirror}}) {
			my ($typemask, $url) = ($value->{typemask}, $value->{mirrorpath});
			if ($typemask >= 4) { $typemask -= 4; push @{$cache->{Mirror}->{xml}}, $url; }
			if ($typemask >= 2) { $typemask -= 2; push @{$cache->{Mirror}->{banner}}, $url; }
			if ($typemask >= 1) { $typemask -= 1; push @{$cache->{Mirror}->{zip}}, $url; }
		}
	}

	# Return hashref of arrays
	return $cache->{Mirror};
}
sub _rand {
	my ($list) = @_;
	# Return random entry from array
	return $list->[int(rand($#$list + 1))];
}
sub chooseMirrors {
	my ($self, $nocache) = @_;
	my $mirrors = $self->getAvailableMirrors($nocache);
	$self->setMirrors(
		&_rand($mirrors->{xml}),
		&_rand($mirrors->{banner}),
		&_rand($mirrors->{zip}),
	);
}

###############################################################################
sub getAvailableLanguages {
	my ($self, $nocache) = @_;

	if ($nocache || not defined $self->{cache}->{Language}) {
		# Download languags XML and process into a hashref
		my $xml = $self->_download($Url{getLanguages}, $self->{apiURL});
		my $data = XMLin($xml, KeyAttr => 'abbreviation');
		$self->{cache}->{Language} = $data->{Language};
	}

	return $self->{cache}->{Language};
}

sub _mtime {
	my ($filename) = @_;
	my @stat = stat($filename);
	return $stat[9];
}

###############################################################################
sub getUpdates {
	my $self = shift;
	my $period = lc shift || 'guess';

	# Determin which update xml file to download
	my $now = time;
	if ($period =~ /^(guess|now)$/) {
		my $diff = $now - $self->{cache}->{Update}->{lastupdated};
		if ($period eq 'guess' && $diff <= $self->{conf}->{minUpdateTime}) {
			# We've updated recently (within 6 hours)
			return;
		} elsif ($diff <= 86400) {	# 1 day in seconds
			$period = 'day';
		} elsif ($diff <= 604800) {	# 1 week in seconds
			$period = 'week';
		} elsif ($diff <= 2592000) {	# 1 month in seconds
			$period = 'month';
		} else  {
			$period = 'all';
		}
	}
	unless ($period =~/^(day|week|month|all)$/) {
		die "Invalid period when calling getUpdates: $period\n";
	}

	# Download appropriate update file
	&verbose(1, "TVDB::API: Downloading $period updates\n");
	my $updates = $self->_downloadZip($Url{getUpdates}, $period);
	return undef unless $updates;

	# Series updates
	my $series = $self->{cache}->{Series};
	while (my ($sid,$data) = each %{$updates->{Series}}) {
		# Don't update if we don't already have this series
		next unless defined $series->{$sid};
		# Only update if there is a more recent version
		if ($data->{time} > $series->{$sid}->{lastupdated}) {
			if ($period eq 'all') {
				# all updates don't include Episodes, so the complete series record is downloaded
				$self->getSeriesAll($sid, 1);
			} else {
				$self->getSeries($sid, 1);
			}
		}
	}

	# Episodes updates
	my $episodes = $self->{cache}->{Episode};
	while (my ($eid,$ep) = each %{$updates->{Episode}}) {
		# Don't update if we don't already have this series
		next unless defined $series->{$ep->{Series}};
		# Get it if we don't already have it
		unless (defined $episodes->{$eid}
			# Or, update if there is a more recent version
			and $ep->{time} > $episodes->{$eid}->{lastupdated}
		) {
			$self->getEpisodeId($eid, 1);
		}
	}

	# Banners updates
	my $banners = $self->{cache}->{Banner};
	if (defined $self->{bannerPath}) {
		for my $banner (@{$updates->{Banner}}) {
			# Don't update if we don't already have this series
			next unless defined $series->{$banner->{Series}};
			# Don't update if we haven't already downloaded this banner
			my $filename = "$self->{bannerPath}/$banner->{path}";
			next unless -f $filename;
			# Don't update if it isn't newer
			next unless -z $filename || $banner->{time} > &_mtime($filename);
			$self->getBanner($banner->{path}, undef, 1);
		}
	}

	# Save when we last updated, now that we've successfully done so
	$self->{cache}->{Update}->{lastupdated} = $now;
	$self->{cache}->{Update}->{lasttime} = $updates->{time};
}

###############################################################################
# Fill in the blank
sub getPossibleSeriesId {
	my ($self, $name) = @_;

	&verbose(2, "TVDB::API: Get possbile series id for $name\n");
	my $xml = $self->_download($Url{getSeriesID}, $Url{defaultURL}, $name, $self->{lang});
	return undef unless $xml;
	my $data = XMLin($xml, ForceArray=>['Series'], KeyAttr=>{});

	# Build hashref to return
	my $ret = {};
	for my $series (@{$data->{Series}}) {
		my $sid = $series->{id};
		if (defined $ret->{$sid}) {
			$ret->{$sid}->{altlanguage} = {};
			$ret->{$sid}->{altlanguage}->{$series->{language}} = $series;
		} else {
			$ret->{$sid} = $series;
		}
	}

	return $ret;
}

###############################################################################
# Fill in the blank
sub getSeriesId {
	my ($self, $name, $nocache) = @_;
	return undef unless defined $name;

	# see if $name is a series id already
	return $name if $name =~ /^\d+$/ && $name > 70000;

	# See if it's in the series cache
	my $cache = $self->{cache};
	if (!$nocache && defined $cache->{Name2Sid}->{$name}) {
		#print "From SID Cache: $name -> $cache->{Name2Sid}->{$name}\n";
		return undef unless $cache->{Name2Sid}->{$name};
		return $cache->{Name2Sid}->{$name};
	}

	my $data = $self->getPossibleSeriesId($name);

	# Look through list of possibilities
	if ($data) {
		while (my ($sid,$series) = each %$data) {
			if ($series->{SeriesName} =~ /^(The )?\Q$name\E(, The)?$/i) {
				$cache->{Name2Sid}->{$name} = $sid;
				return $sid;
			}
		}
	}

	# Nothing found, assign 0 to name so we cache this result
	&warning("TBDB::API: No series id found for: $name\n");
	$cache->{Name2Sid}->{$name} = 0; # Not undef as that messes up DBM::Deep
	return undef;
}

###############################################################################
# Get series/lang.xml for series
sub getSeries {
	my ($self, $name, $nocache) = @_;
	&debug(2, "getSeries: $name, $nocache\n");

	my $sid = $self->getSeriesId($name, $nocache?$nocache-1:0);
	return undef unless $sid;

	my $series = $self->{cache}->{Series};
	if (defined $series->{$sid} && $series->{$sid}->{Seasons}) {
		# Get updated series data
		if ($nocache) {
			&verbose(1, "TVDB::API: Updating series: $sid => $series->{$sid}->{SeriesName}\n");
			my $data = $self->_downloadXml($Url{getSeries}, $sid, $self->{lang});
			return undef unless $data;

			# Copy updated series into cache
			while (my ($key,$value) = each %{$data->{Series}->{$sid}}) {
				$series->{$sid}->{$key} = $value;
			}

		# From cache
		} else {
			&debug(2, "From Series Cache: $sid\n");
		}

	# Get full series data
	} else {
		$self->getSeriesAll($sid, 1);
	}

	return $series->{$sid};
}

###############################################################################
# Get series/all/lang.zip for series
sub getSeriesAll {
	my ($self, $name, $nocache) = @_;
	&debug(2, "getSeriesAll: $name, $nocache\n");

	my $sid = $self->getSeriesId($name, $nocache?$nocache-1:0);
	return undef unless $sid;

	# Get series data
	my $series = $self->{cache}->{Series};
	if (!$nocache && defined $series->{$sid} && $series->{$sid}->{Seasons}) {
		&debug(2, "From Series Cache: $sid\n");

	# Download full series data
	} else {
		&verbose(1, "TVDB::API: Downloading full series: $sid".(defined $series->{$sid}?" => $series->{$sid}->{SeriesName}":'')."\n");
		my $data = $self->_downloadZip($Url{getSeriesAll}, $sid, $self->{lang});
		return undef unless $data;

		# Copy series into cache
		#@{$series->{$sid}}{keys %{$data->{Series}->{$sid}}} = values %{$data->{Series}->{$sid}};
		if (defined $series->{$sid}) {
			while (my ($key,$value) = each %{$data->{Series}->{$sid}}) {
				$series->{$sid}->{$key} = $value;
			}
		} else {
			$self->{cache}->{Series}->{$sid} = $data->{Series}->{$sid};
		}

		# Copy episodes into cache
		while (my ($eid,$ep) = each %{$data->{Episode}}) {
			$series->{$sid}->{Seasons} = [] unless $series->{$sid}->{Seasons};
			#print "Season: $ep->{SeasonNumber} $series->{$sid}->{Seasons}->[$ep->{SeasonNumber}]\n";
			$series->{$sid}->{Seasons}->[$ep->{SeasonNumber}]->[$ep->{EpisodeNumber}] = $eid;
			$self->{cache}->{Episode}->{$eid} = $ep;
		}

		# Save actors
		$series->{$sid}->{Actor} = $data->{Actor};

		# Save banners
		$series->{$sid}->{Banner} = $data->{Banner};
	}

	return $series->{$sid};
}

###############################################################################
sub getSeriesName {
	my ($self, $sid, $nocache) = @_;

	my $series = $self->getSeries($sid, $nocache);
	return undef unless $series;

	return $series->{SeriesName};
}

###############################################################################
# Get series/actors.xml for Series
sub getSeriesActors {
	my ($self, $name, $nocache) = @_;

	my $sid = $self->getSeriesId($name, $nocache?$nocache-2:0);
	return undef unless $sid;
	my $series = $self->getSeries($sid, $nocache?$nocache-1:0);
	return undef unless $series;

	# Get actors data
	if ($nocache or not $series->{Actor}) {
		&verbose(1, "TVDB::API: Get actors: $series->{SeriesName}\n");
		my $data = $self->_downloadXml($Url{getSeriesActors}, $sid);
		return undef unless $data;

		# Copy updated series into cache
		$self->{cache}->{Series}->{$sid}->{Actor} = $data->{Actor};

	# From cache
	} else {
		&debug(2, "From Actors Cache: $series->{SeriesName}\n");
	}

	return $series->{Actor};
}

###############################################################################
sub getSeriesActorsSorted {
	my ($self, $name, $nocache) = @_;
	my $data = $self->getSeriesActors($name, $nocache);
	my @sorted = sort {
		$a->{SortOrder} <=> $b->{SortOrder}
		&& $a->{Role} cmp $b->{Role}
		&& $a->{Name} cmp $b->{Name}
	} values %$data;
	return \@sorted;
}

###############################################################################
# Get series/banners.xml for Series
sub getSeriesBanners {
	my ($self, $name, $type, $type2, $value, $nocache) = @_;

	my $sid = $self->getSeriesId($name, $nocache?$nocache-2:0);
	return undef unless $sid;
	my $series = $self->getSeries($sid, $nocache?$nocache-1:0);
	return undef unless $series;

	# Get banner data
	if ($nocache or not $series->{Banner}) {
		&verbose(1, "TVDB::API: Get banners: $series->{SeriesName}\n");
		my $data = $self->_downloadXml($Url{getSeriesBanner}, $sid);
		return undef unless $data;

		# Copy updated series into cache
		$self->{cache}->{Series}->{$sid}->{Banner} = $data->{Banner};

	# From cache
	} else {
		&debug(2, "From Banners Cache: $series->{SeriesName}\n");
	}

	# Search banners
	my %banners;
	while (my ($id,$banner) = each %{$series->{Banner}}) {
		next unless $banner->{Language} =~ /$self->{lang}|en/;
		next unless !$type || $banner->{BannerType} eq $type;
		next unless !$type2 || $banner->{BannerType2} eq $type2;
		next unless !$value || $type eq 'season' && $banner->{Season} eq $value;
		$banners{$id} = $banner;
	}

	return \%banners;
}

###############################################################################
# Get info for Series
sub getSeriesInfo {
	my ($self, $name, $info, $nocache) = @_;

	my $data = $self->getSeries($name, $nocache);
	return undef unless $data;

	# Check that info is available
	unless (defined $data->{$info}) {
		#&warning("TBDB::API: No $info found for series $name\n");
		return undef;
	}

	return $data->{$info};
}

###############################################################################
sub getSeriesBanner {
	my ($self, $name, $buffer, $nocache) = @_;
	my $banner = $self->getSeriesInfo($name, 'banner', $nocache?$nocache-1:0);
	return undef unless $banner;
	return $self->getBanner($banner, $buffer, $nocache);
}
sub getSeriesFanart {
	my ($self, $name, $buffer, $nocache) = @_;
	my $banner = $self->getSeriesInfo($name, 'fanart', $nocache?$nocache-1:0);
	return undef unless $banner;
	return $self->getBanner($banner, $buffer, $nocache);
}
sub getSeriesPoster {
	my ($self, $name, $buffer, $nocache) = @_;
	my $banner = $self->getSeriesInfo($name, 'poster', $nocache?$nocache-1:0);
	return undef unless $banner;
	return $self->getBanner($banner, $buffer, $nocache);
}
sub getSeriesOverview {
	my ($self, $name, $nocache) = @_;
	return $self->getSeriesInfo($name, 'Overview', $nocache);
}

###############################################################################
sub _makedir {
	my $dir = shift;
	return unless $dir;

	# mkdir piece at a time
	unless( -d $dir ) {
		my $path;
		for my $part (split '/', $dir) {
			$path .= "$part/";
			unless (-e $path) {
				&debug([2,2,1], "mkdir $path\n");
				mkdir $path;
			}
		}
	}
}

###############################################################################
# get named banner. Download if not already. Read from cache if buffer provided.
sub getBanner {
	my ($self, $banner, $buffer, $nocache) = @_;

	return unless defined $self->{bannerPath};

	my $filename = "$self->{bannerPath}/$banner";

	# See if we tried to get this during the last week and failed
	if (-z $filename && (time - &_mtime($filename) < $self->{conf}->{minBannerTime})) {
		&verbose(2, "TVDB::API: download of $banner failed before\n");
		return undef;
	}

	if ($nocache || ! -s $filename) {
		my $buf;
		my $gfx = $buffer ? $buffer : \$buf;

		# Download banner (create zero length file if nothing downloaded)
		&verbose(1, "TVDB::API: Get banner $banner\n");
		$$gfx = $self->_download($self->{bannerURL}.$banner);
		&_makedir($1) if $filename =~ m|^(.*)/[^/]+$|;
		open(GFX, "> $filename") || die "$filename:$!";
		print GFX $$gfx;
		return undef unless $$gfx;

	} elsif ($buffer && -s $filename) {
		# get Banner from cache
		&debug(2, "From Banner Cache: $banner\n");
		open(GFX, "< $filename") || die "$filename:$!";
		local $/ = undef;
		$$buffer = <GFX>;
	}
	close GFX;

	return $banner;
}

###############################################################################
sub getMaxSeason {
	my ($self, $name, $nocache) = @_;
	$self->getUpdates(); # Update available episodes/seasons
	my $series = $self->getSeriesAll($name, $nocache?$nocache-1:0);
	return undef unless $series;
	return $#{$series->{Seasons}};
}

###############################################################################
sub getSeason {
	my ($self, $name, $season, $nocache) = @_;
	if ($season < 0 || $season > $self->{conf}->{maxSeason}) {
		&warning("TBDB::API: Invalid season $season for $name\n");
		return undef;
	}
	my $series = $self->getSeriesAll($name, $nocache?$nocache-1:0);
	return undef unless $series && $series->{Seasons};
	unless ($series->{Seasons}->[$season]) {
		$self->getUpdates();
		unless ($series->{Seasons}->[$season]) {
			&warning("TBDB::API: No season $season found for $name\n");
			#$series->{Seasons}->[$season] = 0;
			return undef;
		}
	}
	return $series->{Seasons}->[$season];
}

###############################################################################
sub getSeasonBanners {
	my ($self, $name, $season, $nocache) = @_;
	my $data = $self->getSeriesBanners($name, 'season', 'season', $season, $nocache);
	my @banners;
	while (my ($id,$banner) = each %$data) {
		push @banners, $banner->{BannerPath};
	}
	return sort @banners;
}
sub getSeasonBanner {
	my ($self, $name, $season, $buffer, $nocache) = @_;
	my @banners = $self->getSeasonBanners($name, $season, $nocache?$nocache-1:0);
	return undef unless @banners;
	return $self->getBanner($banners[0], $buffer, $nocache);
}

###############################################################################
sub getSeasonBannersWide {
	my ($self, $name, $season, $nocache) = @_;
	my $data = $self->getSeriesBanners($name, 'season', 'seasonwide', $season, $nocache);
	my @banners;
	while (my ($id,$banner) = each %$data) {
		push @banners, $banner->{BannerPath};
	}
	return sort @banners;
}
sub getSeasonBannerWide {
	my ($self, $name, $season, $buffer, $nocache) = @_;
	my @banners = $self->getSeasonBannersWide($name, $season, $nocache?$nocache-1:0);
	return undef unless @banners;
	return $self->getBanner($banners[0], $buffer, $nocache);
}

###############################################################################
sub getMaxEpisode {
	my ($self, $name, $season, $nocache) = @_;
	$self->getUpdates(); # Update available episodes/seasons
	my $data = $self->getSeason($name, $season, $nocache);
	return undef unless $data;
	return $#$data;
}

###############################################################################
sub getEpisode {
	my ($self, $name, $season, $episode, $nocache) = @_;
	if ($episode < 0 || $episode > $self->{conf}->{maxEpisode}) {
		&warning("TBDB::API: Invalid episode $episode in season $season for $name\n");
		return undef;
	}
	my $sid = $self->getSeriesId($name);
	my $data = $self->getSeason($sid, $season, $nocache?$nocache-1:0);
	return undef unless $data;

	# See if we have to update the episode record
	my $cache = $self->{cache};
	my $series = $cache->{Series};
	my $eid = $data->[$episode] if defined $data->[$episode];
	if (ref($eid) ne '' && (time - $eid->{lasttried}) < $self->{conf}->{minEpisodeTime}) {
		&verbose(2, "TBDB::API: No episode $episode found for season $season of $name (cached)\n");
		return undef;
	}
	unless (!$nocache && $eid && !ref($eid) && $cache->{Episode}->{$eid}) {
		# Download episode
		&verbose(1, "TVDB::API: Updating episode $episode from season $season for $name\n");
		my $new = $self->_downloadXml($Url{getEpisode}, $sid, $season, $episode, $self->{lang});

		if ($new) {
			# Save episode in cache
			($eid, my $ep) = each %{$new->{Episode}};
			$series->{$sid}->{Seasons} = [] unless $series->{$sid}->{Seasons};
			$series->{$sid}->{Seasons}->[$season]->[$episode] = $eid;
			$cache->{Episode}->{$eid} = $ep;
		} else {
			$eid = 0;
			$series->{$sid}->{Seasons}->[$season]->[$episode] = {};
			$series->{$sid}->{Seasons}->[$season]->[$episode]->{lasttried} = time;
		}
	}

	# Check again (if it's been updated)
	unless ($eid && defined $cache->{Episode}->{$eid}) {
		&warning("TBDB::API: No episode $episode found for season $season of $name\n");
		return undef;
	}

	return $cache->{Episode}->{$eid};
}

###############################################################################
sub getEpisodeAbs {
	my ($self, $name, $abs, $nocache) = @_;
	if ($abs < 0 || $abs > $self->{conf}->{maxEpisode}*$self->{conf}->{maxSeason}) {
		&warning("TBDB::API: Invalid absolute episode $abs for $name\n");
		return undef;
	}
	my $sid = $self->getSeriesId($name);
	return undef unless $sid;
	my $series = $self->getSeriesAll($sid, $nocache?$nocache-1:0);
	return undef unless $series;

	# Look for episode in cache
	my $cache = $self->{cache};
	unless ($nocache) {
		foreach my $season (@{$series->{Seasons}}) {
			foreach my $eid (@$season) {
				next unless $eid;
				my $ep = $cache->{Episode}->{$eid};
				return $ep if $ep->{absolute_number} eq $abs;
			}
		}
	}

	# Download absolute episode
	&verbose(1, "TVDB::API: Updating absolute episode $abs for $name\n");
	my $new = $self->_downloadXml($Url{getEpisodeAbs}, $sid, $abs, $self->{lang});
	if ($new) {
		# Save episode in cache
		my ($eid, $ep) = each %{$new->{Episode}};
		$series->{$sid}->{Seasons} = [] unless $series->{$sid}->{Seasons};
		$series->{$sid}->{Seasons}->[$ep->{SeasonNumber}]->[$ep->{EpisodeNumber}] = $eid;
		$cache->{Episode}->{$eid} = $ep;
		return $cache->{Episode}->{$eid};
	}

	&warning("TBDB::API: No absolute episode $abs found for $name\n");
	return undef;
}

###############################################################################
sub getEpisodeDVD {
	my ($self, $name, $season, $episode, $nocache) = @_;
	my $epmajor = int($episode);
	if ($epmajor < 0 || $epmajor > $self->{conf}->{maxEpisode}) {
		&warning("TBDB::API: Invalid DVD episode $episode in DVD season $season for $name\n");
		return undef;
	}
	my $sid = $self->getSeriesId($name);
	return undef unless $sid;
	my $data = $self->getSeason($sid, $season, $nocache?$nocache-1:0);
	return undef unless $data;

	# Look for episode in cache
	my $cache = $self->{cache};
	my $series = $cache->{Series};
	unless ($nocache) {
		foreach my $eid (@$data) {
			next unless $eid;
			my $ep = $cache->{Episode}->{$eid};
			my $de = $ep->{DVD_episodenumber};
			return $ep if $de eq $episode
		   			|| int($de) eq $episode
		   			|| int($de) eq $epmajor;
		}
	}

	# Download DVD episode
	&verbose(1, "TVDB::API: Updating DVD episode $episode from DVD season $season for $name\n");
	my $new = $self->_downloadXml($Url{getEpisodeDVD}, $sid, $season, $episode, $self->{lang});
	if ($new) {
		# Save episode in cache
		my ($eid, $ep) = each %{$new->{Episode}};
		$series->{$sid}->{Seasons} = [] unless $series->{$sid}->{Seasons};
		$series->{$sid}->{Seasons}->[$ep->{SeasonNumber}]->[$ep->{EpisodeNumber}] = $eid;
		$cache->{Episode}->{$eid} = $ep;
		return $cache->{Episode}->{$eid};
	}

	&warning("TBDB::API: No DVD episode $episode found for DVD season $season of $name\n");
	return undef;
}

###############################################################################
sub getEpisodeId {
	my ($self, $eid, $nocache) = @_;
	my $cache = $self->{cache};
	unless (!$nocache && defined $cache->{Episode}->{$eid}) {
		# Download episode
		&verbose(1, "TVDB::API: Updating episode id $eid\n");
		my $new = $self->_downloadXml($Url{getEpisodeID}, $eid, $self->{lang});
		return undef unless $new;

		# Save episode in cache
		$cache->{Episode}->{$eid} = $new->{Episode}-{$eid};
	}

	return $cache->{Episode}->{$eid};
}

###############################################################################
sub getEpisodeByAirDate {
	my ($self, $name, $airdate, $nocache) = @_;
	my $sid = $self->getSeriesId($name, $nocache?$nocache-1:0);

	my $cache = $self->{cache};

	# Download episode
	&verbose(1, "TVDB::API: Get episode for $name ($sid) on $airdate\n");
	my $new = $self->_downloadApikeyXml($Url{getEpisodeByAirDate}, $sid, $airdate, $self->{lang});
	return undef unless $new;

	return $new->{Episode};
}

###############################################################################
sub getEpisodeInfo {
	my ($self, $name, $season, $episode, $info, $nocache) = @_;

	my $data = $self->getEpisode($name, $season, $episode, $nocache);
	return undef unless $data;

	# Check that info is available
	unless (defined $data->{$info}) {
		#&warning("TBDB::API: No $info found for episode $episode of season $season of $name\n");
		return undef;
	}

	return $data->{$info};
}

###############################################################################
sub getEpisodeBanner {
	my ($self, $name, $season, $episode, $buffer, $nocache) = @_;
	my $banner = $self->getEpisodeInfo($name, $season, $episode, 'filename', $nocache?$nocache-1:0);
	return undef unless $banner;
	return $self->getBanner($banner, $buffer, $nocache);
}
sub getEpisodeName {
	my ($self, $name, $season, $episode, $nocache) = @_;
	return $self->getEpisodeInfo($name, $season, $episode, 'EpisodeName', $nocache);
}
sub getEpisodeOverview {
	my ($self, $name, $season, $episode, $nocache) = @_;
	return $self->getEpisodeInfo($name, $season, $episode, 'Overview', $nocache);
}

###############################################################################
sub getRatingsForUser {
	my ($self, $user, $name, $nocache) = @_;

	# Download ratings
	my $data;
	if ($name) {
		my $sid = $self->getSeriesId($name, $nocache?$nocache-1:0);
		&verbose(1, "TVDB::API: Get rating for $user for $name ($sid)\n");
		$data = $self->_downloadApikeyXml($Url{getRatingsForUser}, $user, $sid);
	} else {
		&verbose(1, "TVDB::API: Get rating for $user\n");
		$data = $self->_downloadApikeyXml($Url{getRatingsForUserAll}, $user);
	}
	return undef unless $data;

	return $data;
}

###############################################################################
__END__