# 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.36";

# 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		=> 100,
	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 (defined $args->{mirrors} && @{$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};

	$self->{now} = time;
	&debug(1, "new TVDB::API (%d)\n", $self->{now});

	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) = @_;
	if(defined($self->{mirror}) and defined($self->{apikey})) {
	    $self->{apiURL} = sprintf $Url{apiURL}, $self->{mirror}, $self->{apikey};
	}
	
	if(defined($self->{banner})) {
        $self->{bannerURL} = sprintf $Url{bannerURL}, $self->{banner};
    }
	
	if(defined($self->{zip}) and defined($self->{apikey})) {
        $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);

	#$url =~ s/\$/%24/g;
	$url =~ s/#/%23/g;
	#$url =~ s/\*/%2A/g;
	#$url =~ s/\!/%21/g;
	&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;

	$xml = Compress::Zlib::memGunzip($xml) unless $xml =~ /^</;

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

	# Return process XML into hashref
	return undef unless $xml;
	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 = Compress::Zlib::memGunzip($xml) unless $xml =~ /^</;

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

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

	# Return process XML into hashref
	return undef unless $xml;
	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";
	my $xml;
	{
		local $/ = undef;
		$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;

    # Outputs XML, (fills the screen up)
	#&debug(4, "download Zip: $url\n", XML => \$xml);

	# Return process XML into hashref
	return undef unless $xml;
	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 getUpdatePeriod {
	my $self = shift;
	my $time = shift || $self->{now};

	# Determine which update xml file to download
	my $diff = $time - $self->{cache}->{Update}->{lastupdated};
	if ($diff <= $self->{conf}->{minUpdateTime}) {
		# We've updated recently (within 6 hours)
		&verbose(2, "TVDB::API: guessed none\n");
		return 'none';
	} elsif ($diff <= 86400) {	# 1 day in seconds
	    &verbose(2, "TVDB::API: guessed day\n");
		return 'day';
	} elsif ($diff <= 604800) {	# 1 week in seconds
	    &verbose(2, "TVDB::API: guessed week\n");
		return 'week';
	} elsif ($diff <= 2592000) {	# 1 month in seconds
	    &verbose(2, "TVDB::API: guessed month\n");
		return 'month';
	}
	&verbose(2, "TVDB::API: guessed all\n");
	return 'all';
}

###############################################################################
sub getSeriesUpdate {
	my $self = shift;
	my $sid = $self->getSeriesId(shift);
	my $time = shift || $self->{now};
	my $all = shift || 0;

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

###############################################################################
sub getEpisodeUpdate {
	my $self = shift;
	my $eid = shift;
	my $time = shift || $self->{now};

	# Episodes updates
	my $episodes = $self->{cache}->{Episode};

	# Earlier versions of this library could have stored numbers
	# in the episode field. If we find these, silently force
	# the cached data to be 'old' which will cause it to be
	# downloaded again.
	if (defined $episodes->{$eid} && !ref $episodes->{$eid}) {
		$episodes->{$eid} = { lastupdated => 0 };
	}

	# Ignore it if we don't already have it in the cache
	next unless defined $episodes->{$eid};

	# Update if there is a more recent version
	if ($time - $episodes->{$eid}->{lastupdated} <= $self->{conf}->{minUpdateTime}) {
		$self->getEpisodeId($eid, 1);
	}
}

###############################################################################
sub getBannerUpdate {
	my $self = shift;
	my $banner = shift;
	my $time = shift || $self->{now};

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

	# Don't update if we haven't already downloaded this banner
	return unless -f $filename;

	# Don't update if it isn't newer
	return unless -z $filename || $time > &_mtime($filename);
	if (ref($banner) eq 'HASHREF') {
		$self->getBanner($banner->{path}, undef, 1);
	} else {
		$self->getBanner($banner, undef, 1);
	}
}

###############################################################################
sub getUpdates {
	my $self = shift;
	my $period = shift || 'guess';
	$period = lc $period;
	
	&verbose(2, "TVDB::API: updates...\n");

	# Determine which update xml file to download
	$period = $self->getUpdatePeriod($self->{now}) if $period =~ /^(guess|now)$/;
	return if $period eq 'none';
	if ($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
	while (my ($sid,$data) = each %{$updates->{Series}}) {
		$self->getSeriesUpdate($sid, $data->{time});
	}

	# Episodes updates
	while (my ($eid,$ep) = each %{$updates->{Episode}}) {
		next unless $self->haveSeries($ep->{Series});
		$self->getEpisodeUpdate($eid, $ep->{time});
	}

	# Banners updates
	my $series = $self->{cache}->{Series};
	if (defined $self->{bannerPath}) {
		for my $banner (@{$updates->{Banner}}) {
			next unless $self->haveSeries($banner->{Series});
			$self->getBannerUpdate($banner->{path}, $banner->{time});
		}
	}

	# Save when we last updated, now that we've successfully done so
	&verbose(2, "TVDB::API: lasttime $updates->{time} - now $self->{now}\n");
	$self->{cache}->{Update}->{lastupdated} = $self->{now};
	$self->{cache}->{Update}->{lasttime} = $updates->{time};
}

###############################################################################
# Find all possible series that are close
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;
}

###############################################################################
# Get ID for named series
sub getSeriesId {
	my ($self, $name, $nocache) = @_;
	$name = $name->[0] if ref $name eq 'ARRAY';
	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("TVDB::API: No series id found for: $name\n");
	$cache->{Name2Sid}->{$name} = 0; # Not undef as that messes up DBM::Deep
	return undef;
}

###############################################################################
# Set Series Id in Cache
sub setSeriesId {
	my ($self, $name, $sid) = @_;
	return 1 unless defined $name;

	# See if it's in the series cache
	my $cache = $self->{cache};
	$cache->{Name2Sid}->{$name} = $sid;
	return 0;
}

###############################################################################
# Do we have this Series?
sub haveSeries {
	my $self = shift;
	my $sid = $self->getSeriesId(shift);
	$sid = $sid->[0] if (ref $sid eq 'ARRAY');
	return undef unless $sid;
	return defined $self->{cache}->{Series}->{$sid};
}

###############################################################################
# Get series/lang.xml for series
sub getSeries {
	my ($self, $name, $nocache) = @_;
	
	# If no name is found, return undef, as the language
	# is probably wrong and is not translated (mostly norweigan series)
	if(!defined($name)) {
		&verbose(1, "TVDB::API: No translated seriesname found - try to change the language from $self->{lang}.\n");
		return undef;
	}
	
	&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;
		#print Dumper($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: $eid $ep->{SeasonNumber} $ep->{EpisodeNumber} ".Dumper($series->{$sid}->{Seasons}->[$ep->{SeasonNumber}]);
			$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("TVDB::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 && ($self->{now} - &_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:$!";
		binmode(GFX);
		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->getSeriesUpdate($name, $self->{now}, 1);
	my $series = $self->getSeriesAll($name, $nocache?$nocache-1:0);
	return undef unless $series;
	return $#{$series->{Seasons}};
}

###############################################################################
sub getSeason {
	my ($self, $name, $season, $nocache) = @_;
	$season = 0 unless $season;
	if ($season < 0 || $season > $self->{conf}->{maxSeason}) {
		&warning("TVDB::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("TVDB::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->getSeriesUpdate($name, $self->{now}, 1); # Update available episodes/seasons
	my $data = $self->getSeason($name, $season, $nocache);
	return undef unless $data;
	my ($max, $maxe) = ($#$data, $self->getConf('maxEpisode')); 
	return $max < $maxe ? $max: $maxe;
}

###############################################################################
sub getMaxEpisodeAbs {
	my ($self, $name, $nocache) = @_;
	my $lastseas = $self->getMaxSeason($name, $nocache?$nocache-1:0);
	return undef unless $lastseas;
	my $data = $self->getSeason($name, $lastseas, $nocache);
	return undef unless $data;

	# Look for episode in cache
	my $max = 0;
	my $eps = $self->{cache}->{Episode};
	foreach my $eid (@$data) {
		next unless $eid;
		my $ep = $eps->{$eid}->{absolute_number};
		$max = $ep if $max < $ep;
	}
	return $max if $max;

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

###############################################################################
sub getMaxEpisodeDVD {
	my ($self, $name, $season, $nocache) = @_;
	my $data = $self->getSeason($name, $season, $nocache);
	return undef unless $data;

	# Look for episode in cache
	my $max = 0;
	my $eps = $self->{cache}->{Episode};
	foreach my $eid (@$data) {
		next unless $eid;
		my $ep = $eps->{$eid}->{DVD_episodenumber};
		$max = $ep if $max < $ep;
	}
	return $max if $max;

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

###############################################################################
sub getEpisode {
	my ($self, $name, $season, $episode, $nocache) = @_;
	$season = 0 unless $season;
	$episode = 0 unless $episode;
	if ($episode < 0 || $episode > $self->{conf}->{maxEpisode}) {
		&warning("TVDB::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 '' && ($self->{now} - $eid->{lasttried}) < $self->{conf}->{minEpisodeTime}) {
		&verbose(2, "TVDB::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} = $self->{now};
		}
	}

	# Check again (if it's been updated)
	unless ($eid && defined $cache->{Episode}->{$eid}) {
		&warning("TVDB::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("TVDB::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("TVDB::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("TVDB::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("TVDB::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 getEpisodeByName {
    my ($self, $name, $episodename, $nocache) = @_;
    if (!defined ($episodename)) {
        &warning("TVDB::API: No episode name defined 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) {
                my $match = lc($episodename);
        foreach my $season (@{$series->{Seasons}}) {
            foreach my $eid (@$season) {
                next unless $eid;
                my $ep = $cache->{Episode}->{$eid};
                next unless $ep->{EpisodeName};
                return $ep if lc($ep->{EpisodeName}) eq $match;
            }
        }
                # try without part number, only accept a single hit (we don't use story arc numbering for uniquely named episodes over here)
        my $hitcount = 0;
        my $hit;
        my $regexpmatch = ($match =~ s|([\Q()\E])|\\$1|);
        foreach my $season (@{$series->{Seasons}}) {
            foreach my $eid (@$season) {
                next unless $eid;
                my $ep = $cache->{Episode}->{$eid};
                next unless $ep->{EpisodeName};
                                if( lc($ep->{EpisodeName}) =~ m|^$regexpmatch \(\d+\)$| ){
                    $hitcount ++;
                    $hit = $ep;
                }
            }
        }
        if( $hitcount == 1){
            return( $hit );
        }

        eval "use Text::LevenshteinXS qw/distance/;";
        if( !$@ ){
            # try with Levenshtein distance 2
            $hitcount = 0;
            foreach my $season (@{$series->{Seasons}}) {
                foreach my $eid (@$season) {
                    next unless $eid;
                    my $ep = $cache->{Episode}->{$eid};
                    next unless $ep->{EpisodeName};
                                 if( distance( lc($ep->{EpisodeName}), $match ) <= 2 ){
                        $hitcount ++;
                        $hit = $ep;
                    }
                }
            }
            if( $hitcount == 1){
                return( $hit );
            }
        }
    }

    # Download named episode
    &verbose(1, "TVDB::API: Would like to update episode named $episodename for $name\n");
# TODO, the site does not provide an API to get one episode by name, yet.
#   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("TVDB::API: No episode named $episodename found for $name\n");
    return undef;
}

###############################################################################
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("TVDB::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__

=head1 NAME

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

=head1 SYNOPSIS

  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 $hashref = $tvdb->getEpisodeByName($series, $episode_name, [$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();

=head1 DESCRIPTION

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

=over 4

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

Create a TVDB::API object using C<APIKEY> and using a default language
of C<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'
                        });

=item setApiKey(APIKEY);

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

=item setLang(LANGUAGE);

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

=item setUserAgent(USERAGENT);

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

=item setBannerPath(PATH);

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

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

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

=item getAvailableMirrors([NOCACHE]);

Get the list of mirror sites available from thetvdb.com. It returns a hashref
of arrays.  If C<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,
  }

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

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

=item chooseMirrors([NOCACHE])

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

=item getConf([KEY])

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

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

Set configurable values by C<KEY>/C<VALUE> pair.  If a hashref is passed in,
all C<KEY>/C<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()

=item getAvailableLanguages([NOCACHE])

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

=item getUpdates([PERIOD])

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

=over 4

=item C<day>

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

=item C<week>

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

=item C<month>

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

=item C<all>

Get all updates available.

=item C<now>

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

=item C<guess>

Like C<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 C<minUpdateTime> with setConf()), do nothing. This is
the default C<PERIOD>.

=back

=item getPossibleSeriesId(SERIESNAME)

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

=item getSeriesId(SERIESNAME, [NOCACHE])

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

=item getSeriesName(SERIESID, [NOCACHE])

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

=item getSeries(SERIESNAME, [NOCACHE])

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

=item getSeriesAll(SERIESNAME, [NOCACHE])

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

=item getSeriesActors(SERIESNAME, [NOCACHE])

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

=item getSeriesActorsSorted(SERIESNAME, [NOCACHE])

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

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

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

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

=item getSeriesInfo(SERIESNAME, KEY, [NOCACHE])

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

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

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

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

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

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

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

=item getSeriesOverview(SERIESNAME, [NOCACHE])

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

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

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

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

=item getMaxSeason(SERIESNAME, [NOCACHE])

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

=item getSeason(SERIESNAME, SEASON, [NOCACHE])

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

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

=item getSeasonBanners(SERIESNAME, SEASON, [NOCACHE])

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

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

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

=item getSeasonBannersWide(SERIESNAME, SEASON, [NOCACHE])

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

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

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

=item getMaxEpisode(SERIESNAME, SEASON, [NOCACHE])

Return the number episodes in C<SEASON> for C<SERIESNAME>.  If C<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 C<maxEpisode> configuration variable determines the maximum allowable
episode (see getConf()/setConf()).

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

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

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

=item getEpisodeAbs(SERIESNAME, ABSEPISODE, [NOCACHE])

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

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

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

=item getEpisodeId(EPISODEID, [NOCACHE])

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

=item getEpisodeByAirDate(SERIESNAME, AIRDATE [NOCACHE])

Return a hashref for the episode in C<SERIESNAME> on C<AIRDATE>. C<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 C<NOCACHE> is non-zero, then
the C<SERIESNAME> to seriesid lookup is downloaded again.

=item getEpisodeByName(SERIESNAME, EPISODENAME, [NOCACHE])

Return a hashref for the episode named (C<EPISODENAME>) for C<SERIESNAME>.
C<NOCACHE> is of no use until the site API is extended. So this call works only
on cached data for now.

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

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

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

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

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

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

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

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

=item getRatingsForUser(USERID, SERIESNAME, [NOCACHE])

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

=item dumpCache()

Dump the cache database with Dumper to stdout.

=back

=head1 EXAMPLE

    use Data::Dumper;
    use TVDB::API;

    my $tvdb = TVDB::API::new($apikey, 'en');
    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'
    };

=head1 AUTHOR

S<Behan Webster E<lt>behanw@websterwood.comE<gt>>

=head1 COPYRIGHT

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.

=cut
