Zachary Kotlarek 9 лет назад
Родитель
Сommit
1f542265df

+ 13 - 3
web/includes/config.php

@@ -1,11 +1,21 @@
 <?
 
-# Set locale to make basename() and others work reasonably
-setlocale(LC_ALL, "en_US.UTF8");
+# Debug output
+define('DEBUG', false);
+
+# Set locale and TZ to make things sane
+define('TZ', 'America/Los_Angeles');
+date_default_timezone_set(TZ);
+setlocale(LC_ALL, 'en_US.UTF8');
+
+# TVDB API
+define('TVDB_URL',     'http://thetvdb.com');
+define('TVDB_API_KEY', '26877109147D171D');
+define('TVDB_DBN',     'pgsql:dbname=tv');
 
 ### Config
 $TV_PATH    = '/mnt/media/TV';	# Root path for all TV series
-$MAX_AGE    = 86400 * 90;		# Consider a series "old" if it has not been updated for this many seconds
+$MAX_AGE    = 86400 * 90;	# Consider a series "old" if it has not been updated for this many seconds
 $CACHE_AGE  = 300;
 $CACHE_FILE = '/var/tmp/php/tv_web.cache';
 

+ 56 - 0
web/includes/tvdb/Actor.php

@@ -0,0 +1,56 @@
+<?php
+
+namespace Moinax\TvDb;
+
+/**
+ * Actor object
+ *
+ * @package TvDb
+ * @author Lucas Personnaz <lucas.personnaz@gmail.com>
+ **/
+
+class Actor 
+{
+
+	/**
+	 * @var int
+	 */
+	public $id;
+
+	/**
+	 * @var string
+	 */
+	public $image;
+
+	/**
+	 * @var string
+	 */
+	public $name;
+
+	/**
+	 * @var string
+	 */
+	public $role;
+
+	/**
+	 * @var int
+	 */
+	public $sortOrder;
+
+	/**
+	 * Constructor
+	 *
+	 * @access public
+	 * @param \SimpleXMLElement $data A simplexml element created from thetvdb.com's xml data for the actor
+	 * @return \Moinax\TvDb\Actor
+	 */
+	public function __construct($data)
+	{
+		$this->id = (int)$data->id;
+		$this->image = (string)$data->Image;
+		$this->name = (string)$data->Name;
+		$this->role = (string)$data->Role;
+		$this->sortOrder = (int)$data->SortOrder;
+	}
+
+}

+ 96 - 0
web/includes/tvdb/Banner.php

@@ -0,0 +1,96 @@
+<?php
+
+namespace Moinax\TvDb;
+
+/**
+ * Simple banner object
+ *
+ * @package TvDb
+ * @author Jérôme Poskin <moinax@gmail.com>
+ */
+class Banner
+{
+
+    /**
+     * @var int
+     */
+    public $id;
+
+    /**
+     * @var string
+     */
+    public $path = '';
+
+    /**
+     * @var string
+     */
+    public $type = '';
+
+    /**
+     * @var string
+     */
+    public $type2 = '';
+
+    /**
+     * @var array
+     */
+    public $colors = array();
+
+    /**
+     * @var string
+     */
+    public $language = '';
+
+    /**
+     * @var float
+     */
+    public $rating = '';
+
+    /**
+     * @var int
+     */
+    public $ratingCount = 0;
+
+    /**
+     * @var string
+     */
+    public $seriesName = '';
+
+    /**
+     * @var string
+     */
+    public $thumbnailPath = '';
+
+    /**
+     * @var string
+     */
+    public $vignettePath = '';
+    
+    /**
+     * @var int
+     */
+    public $season;
+
+    /**
+     * Constructor
+     *
+     * @access public
+     * @param \SimpleXMLElement $data A simplexml element created from thetvdb.com's xml data for the tv serie banner
+     * @return \Moinax\TvDb\Banner
+     */
+    public function __construct($data)
+    {
+        $this->id = (int)$data->id;
+        $this->path = (string)$data->BannerPath;
+        $this->type = (string)$data->BannerType;
+        $this->type2 = (string)$data->BannerType2;
+        $this->colors = (array)$data->Colors;
+        $this->language = (string)$data->Language;
+        $this->rating = (float)$data->Rating;
+        $this->ratingCount = (int)$data->RatingCount;
+        $this->seriesName = (string)$data->SeriesName;
+        $this->thumbnailPath = (string)$data->ThumbnailPath;
+        $this->vignettePath = (string)$data->VignettePath;
+        $this->season = (int)$data->Season;
+    }
+}

+ 544 - 0
web/includes/tvdb/Client.php

@@ -0,0 +1,544 @@
+<?php
+
+namespace Moinax\TvDb;
+
+use Moinax\TvDb\CurlException;
+use Moinax\TvDb\Http\CurlClient;
+use Moinax\TvDb\Http\HttpClient;
+
+/**
+ * Base TVDB library class, provides universal functions and variables
+ *
+ * @package TvDb
+ * @author Jérôme Poskin <moinax@gmail.com>
+ **/
+class Client
+{
+    const POST = 'post';
+    const GET = 'get';
+
+    const MIRROR_TYPE_XML = 1;
+    const MIRROR_TYPE_BANNER = 2;
+    const MIRROR_TYPE_ZIP = 4;
+
+    const DEFAULT_LANGUAGE = 'en';
+
+    const FORMAT_XML = 'xml';
+    const FORMAT_ZIP = 'zip';
+
+    /**
+     * Base url for TheTVDB
+     *
+     * @var string
+     */
+    protected $baseUrl = '';
+
+    /**
+     * API key for thetvdb.com
+     *
+     * @var string
+     */
+    protected $apiKey = '';
+
+    /**
+     * Array of available mirrors
+     *
+     * @var array
+     */
+    protected $mirrors = array();
+
+    /**
+     * Array of available languages
+     *
+     * @var array
+     */
+    protected $languages = array();
+
+    /**
+     * Default language.
+     *
+     * @var string
+     */
+    protected $defaultLanguage = 'en';
+
+    /**
+     * HttpClient used to query the API.
+     *
+     * @var HttpClient
+     */
+    protected $httpClient;
+
+    /**
+     * @param string $baseUrl Domain name of the api without trailing slash
+     * @param string $apiKey Api key provided by http://thetvdb.com
+     */
+    public function __construct($baseUrl, $apiKey)
+    {
+        $this->baseUrl = $baseUrl;
+        $this->apiKey = $apiKey;
+        $this->httpClient = new CurlClient();
+    }
+
+    /**
+     * Set the HttpClient used to query the API.
+     *
+     * @param HttpClient $client
+     */
+    public function setHttpClient(HttpClient $client)
+    {
+        $this->httpClient = $client;
+    }
+
+    /**
+     * Get a language information
+     *
+     * @param string $abbreviation
+     * @return array
+     * @throws \Exception
+     */
+    public function getLanguage($abbreviation)
+    {
+        if (empty($this->languages)) {
+            $this->getLanguages();
+        }
+        if (!isset($this->languages[$abbreviation])) {
+            throw new \Exception('This language is not available');
+        }
+
+        return $this->languages[$abbreviation];
+    }
+
+    /**
+     * Set the default query language. It must be a valid language code.
+     *
+     * @param string $language
+     */
+    public function setDefaultLanguage($language) {
+        $this->defaultLanguage = $language;
+    }
+
+    /**
+     * Get the default query language.
+     *
+     * @return string
+     */
+    public function getDefaultLanguage() {
+        return $this->defaultLanguage;
+    }
+
+    /**
+     * Get the server time for further updates
+     *
+     * @return string
+     */
+    public function getServerTime()
+    {
+        return (string)$this->fetchXml('Updates.php?type=none')->Time;
+    }
+
+    /**
+     * Searches for tv serie based on series name
+     *
+     * @param string $seriesName
+     * @param string $language
+     * @return array
+     */
+    public function getSeries($seriesName, $language = null)
+    {
+        $language = $language ? : $this->defaultLanguage;
+
+        $data = $this->fetchXml('GetSeries.php?seriesname=' . urlencode($seriesName) . '&language=' . $language);
+        $series = array();
+        foreach ($data->Series as $serie) {
+            $series[] = new Serie($serie);
+        }
+        return $series;
+    }
+
+    /**
+     * Find a tv serie by the id from thetvdb.com
+     *
+     * @var int $serieId
+     * @var string $language
+     *
+     * @return Serie A serie object or false if not found
+     **/
+    public function getSerie($serieId, $language = null)
+    {
+        $language = $language ? : $this->defaultLanguage;
+
+        $data = $this->fetchXml('series/' . $serieId . '/' . $language . '.xml');
+
+        return new Serie($data->Series);
+    }
+
+    /**
+     * Find a tv serie by a remote id
+     *
+     * @param array $remoteId
+     * @param string $language
+     *
+     * @example <pre><code>$obj->getSerieByRemoteId(array('imdbid' => '<imdbid>'));</code></pre>
+     * @example <pre><code>$obj->getSerieByRemoteId(array('zap2it' => '<zap2it>'));</code></pre>
+     *
+     * @return Serie
+     */
+    public function getSerieByRemoteId(array $remoteId, $language = null)
+    {
+        $language = $language ? : $this->defaultLanguage;
+
+        $data = $this->fetchXml('GetSeriesByRemoteID.php?' . http_build_query($remoteId) . '&language=' . $language);
+
+        return new Serie($data->Series);
+    }
+
+    /**
+     * Find all banners related to a serie
+     *
+     * @param int $serieId
+     * @return string
+     */
+    public function getBanners($serieId)
+    {
+        $data = $this->fetchXml('series/' . $serieId . '/banners.xml');
+        $banners = array();
+        foreach ($data->Banner as $banner) {
+            $banners[] = new Banner($banner);
+        }
+
+        return $banners;
+    }
+
+    /**
+     * Find banners that match a filter string
+     * Eg: poster or season 
+    */
+    public function getBannersFiltered($serieId,$filter) {
+        $data = $this->getBanners($serieId);
+        $banners = array();
+        foreach($data as $image) {
+            if($image->type == $filter) {
+                $banners[] = $image;
+            }
+        }
+        return $banners;
+    }
+
+    /**
+     * Find all actors related to a serie
+     *
+     * @param int $serieId
+     * @return array
+     */
+    public function getActors($serieId)
+    {
+        $data = $this->fetchXml('series/'. $serieId . '/actors.xml');
+        $actors = array();
+        foreach ($data->Actor as $actor) {
+            $actors [] = new Actor($actor);
+        }
+
+        return $actors;
+    }
+
+    /**
+     * Get all episodes for a serie
+     *
+     * @param int $serieId
+     * @param string $language
+     * @param string $format
+     * @return array
+     * @throws \ErrorException
+     */
+    public function getSerieEpisodes($serieId, $language = null, $format = self::FORMAT_XML)
+    {
+        $language = $language ? : $this->defaultLanguage;
+
+        switch ($format) {
+            case self::FORMAT_XML:
+                $data = $this->fetchXml('series/' . $serieId . '/all/' . $language . '.' . $format);
+                break;
+            case self::FORMAT_ZIP:
+                if (!in_array('zip', stream_get_wrappers())) {
+                    throw new \ErrorException('Your PHP does nort support ZIP stream wrappers');
+                }
+                $data = $this->fetchZIP('series/' . $serieId . '/all/' . $language . '.' . $format, array(), self::GET, $language.".xml");
+                break;
+            default:
+                throw new \ErrorException('Unsupported format');
+                break;
+        }
+        $serie = new Serie($data->Series);
+        $episodes = array();
+        foreach ($data->Episode as $episode) {
+            $episodes[(int)$episode->id] = new Episode($episode);
+        }
+        return array('serie' => $serie, 'episodes' => $episodes);
+    }
+
+    /**
+     * Get a specific episode by season and episode number
+     *
+     * @var int $serieId required the id of the serie
+     * @var int $season required the season number
+     * @var int $episode required the episode number
+     * @var string $language language abbreviation
+     *
+     * @return Episode
+     **/
+    public function getEpisode($serieId, $season, $episode, $language = null)
+    {
+        $language = $language ? : $this->defaultLanguage;
+
+        $data = $this->fetchXml('series/' . $serieId . '/default/' . $season . '/' . $episode . '/' . $language . '.xml');
+
+        return new Episode($data->Episode);
+    }
+
+    /**
+     * Get a specific episode by his id
+     *
+     * @var int $episodeId required the id of the episode
+     * @var string $language
+     * @return Episode
+     **/
+    public function getEpisodeById($episodeId, $language = null)
+    {
+        $language = $language ? : $this->defaultLanguage;
+
+        $data = $this->fetchXml('episodes/' . $episodeId . '/' . $language . '.xml');
+
+        return new Episode($data->Episode);
+    }
+    
+    /**
+     * @param      $serieId
+     * @param      $airdate
+     * @param null $language
+     *
+     * @return Episode
+     */
+    public function getEpisodeByAirDate($serieId, $airdate, $language = null)
+    {
+        $language = $language ? : $this->defaultLanguage;
+        $data = $this->fetchXml('GetEpisodeByAirDate.php?apikey=' . $this->apiKey . '&seriesid=' . $serieId . '&airdate=' . $airdate . '&language=' . $language );
+        return new Episode($data->Episode);
+    }
+
+    /**
+     * Get updates list based on previous time you got data
+     *
+     * @param int $previousTime
+     * @return array
+     */
+    public function getUpdates($previousTime)
+    {
+        $data = $this->fetchXml('Updates.php?type=all&time=' . $previousTime);
+
+        $series = array();
+        foreach ($data->Series as $serieId) {
+            $series[] = (int)$serieId;
+        }
+        $episodes = array();
+        foreach ($data->Episode as $episodeId) {
+            $episodes[] = (int)$episodeId;
+        }
+        return array('series' => $series, 'episodes' => $episodes);
+    }
+
+
+    /**
+     * Fetch banner raw jpeg data from banner mirror.
+     *
+     * @param string $banner the banner to fetch like graphical/79349-g7.jpg
+     * @return string raw jpeg data
+     */
+    public function fetchBanner($banner)
+    {
+        $url = $this->getMirror(self::MIRROR_TYPE_BANNER) . '/banners/' . $banner;
+        return $this->httpClient->fetch($url, array(), self::GET);
+    }
+
+
+    /**
+     * Fetches data via curl and returns result
+     *
+     * @access protected
+     * @param string $function The function used to fetch data in xml
+     * @param array $params
+     * @param string $method
+     * @return string The data
+     */
+    protected function fetchXml($function, $params = array(), $method = self::GET)
+    {
+        if (strpos($function, '.php') > 0) { // no need of api key for php calls
+            $url = $this->getMirror(self::MIRROR_TYPE_XML) . '/api/' . $function;
+        } else {
+            $url = $this->getMirror(self::MIRROR_TYPE_XML) . '/api/' . $this->apiKey . '/' . $function;
+        }
+
+        $data = $this->httpClient->fetch($url, $params, $method);
+
+        $simpleXml = $this->getXml($data);
+
+        return $simpleXml;
+    }
+
+    /**
+     * Fetches data via curl and returns result
+     *
+     * @access protected
+     * @param string $function The function used to fetch data in zip
+     * @param array $params
+     * @param string $method
+     * @param string $file The file to extract from the ZIP
+     * @return string The data
+     */
+    protected function fetchZIP($function, $params = array(), $method = self::GET, $file=null)
+    {
+        if (strpos($function, '.php') > 0) { // no need of api key for php calls
+            $url = $this->getMirror(self::MIRROR_TYPE_ZIP) . '/api/' . $function;
+        } else {
+            $url = $this->getMirror(self::MIRROR_TYPE_ZIP) . '/api/' . $this->apiKey . '/' . $function;
+        }
+
+        $zipName = tempnam(sys_get_temp_dir(), "tvdb-");
+        $zip = fopen($zipName, "w");
+        fwrite($zip, $this->httpClient->fetch($url, $params, $method));
+        fclose($zip);
+        if (is_null($file)) {
+            $file = $this->getDefaultLanguage().".xml";
+        }
+        $dataPath = "zip://".$zipName."#".$file;
+        $data = file_get_contents($dataPath);
+
+        $simpleXml = $this->getXml($data);
+
+        return $simpleXml;
+    }
+
+    /**
+     * Fetch data with curl
+     *
+     * @param string $url
+     * @param array $params
+     * @param string $method
+     * @throws CurlException
+     * @return bool|string
+     */
+    protected function fetch($url, array $params = array(), $method = self::GET)
+    {
+        return $this->httpClient->fetch($url, $params, $method);
+    }
+
+    /**
+     * Convert xml string to SimpleXMLElement
+     *
+     * @param string $data
+     * @return \SimpleXMLElement
+     * @throws \ErrorException|\Exception
+     */
+    protected function getXml($data)
+    {
+        if (extension_loaded('libxml')) {
+            libxml_use_internal_errors(true);
+        }
+
+        $simpleXml = simplexml_load_string($data);
+        if (!$simpleXml) {
+            if (extension_loaded('libxml')) {
+                $xmlErrors = libxml_get_errors();
+                $errors = array();
+                foreach ($xmlErrors as $error) {
+                    $errors[] = sprintf('Error in file %s on line %d with message : %s', $error->file, $error->line, $error->message);
+                }
+                if (count($errors) > 0) {
+
+                    throw new XmlException(implode("\n", $errors));
+                }
+            }
+            throw new XmlException('Xml file could not be loaded');
+        }
+
+        return $simpleXml;
+    }
+
+    /**
+     * Get a list of mirrors available to fetchXml the data from the api
+     * @return void
+     */
+    protected function getMirrors()
+    {
+        $data = $this->httpClient->fetch($this->baseUrl . '/api/' . $this->apiKey . '/mirrors.xml');
+        $mirrors = $this->getXml($data);
+
+        foreach ($mirrors->Mirror as $mirror) {
+            $typeMask = (int)$mirror->typemask;
+            $mirrorPath = (string)$mirror->mirrorpath;
+
+            if ($typeMask & self::MIRROR_TYPE_XML) {
+                $this->mirrors[self::MIRROR_TYPE_XML][] = $mirrorPath;
+            }
+            if ($typeMask & self::MIRROR_TYPE_BANNER) {
+                $this->mirrors[self::MIRROR_TYPE_BANNER][] = $mirrorPath;
+            }
+            if ($typeMask & self::MIRROR_TYPE_ZIP) {
+                $this->mirrors[self::MIRROR_TYPE_ZIP][] = $mirrorPath;
+            }
+        }
+    }
+
+    /**
+     * Get a random mirror from the list of available mirrors
+     *
+     * @param int $typeMask
+     * @return string
+     * @access protected
+     */
+    public function getMirror($typeMask = self::MIRROR_TYPE_XML)
+    {
+        if (empty($this->mirrors)) {
+            $this->getMirrors();
+        }
+        return $this->mirrors[$typeMask][array_rand($this->mirrors[$typeMask], 1)];
+
+    }
+
+    /**
+     * Get a list of languages available for the content of the api
+     * @return \SimpleXMLElement
+     */
+    protected function getLanguages()
+    {
+        $languages = $this->fetchXml('languages.xml');
+
+        foreach ($languages->Language as $language) {
+            $this->languages[(string)$language->abbreviation] = array(
+                'name' => (string)$language->name,
+                'abbreviation' => (string)$language->abbreviation,
+                'id' => (int)$language->id,
+            );
+        }
+    }
+
+    /**
+     * Removes indexes from an array if they are zero length after trimming
+     *
+     * @param array $array The array to remove empty indexes from
+     * @return array An array with all empty indexes removed
+     **/
+    public static function removeEmptyIndexes($array)
+    {
+
+        $length = count($array);
+
+        for ($i = $length - 1; $i >= 0; $i--) {
+            if (trim($array[$i]) == '') {
+                unset($array[$i]);
+            }
+        }
+
+        sort($array);
+        return $array;
+    }
+}

+ 9 - 0
web/includes/tvdb/CurlException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Moinax\TvDb;
+
+
+class CurlException extends \Exception
+{
+
+}

+ 126 - 0
web/includes/tvdb/Episode.php

@@ -0,0 +1,126 @@
+<?php
+
+namespace Moinax\TvDb;
+
+/**
+ * Episode class. Class for single tv episode for a TV serie.
+ *
+ * @package TvDb
+ * @author Jérôme Poskin <moinax@gmail.com>
+ **/
+class Episode
+{
+
+    /**
+     * @var int
+     */
+    public $id = 0;
+
+    /**
+     * @var int
+     */
+    public $number = 0;
+
+    /**
+     * @var int
+     */
+    public $season = 0;
+
+    /**
+     * @var array
+     */
+    public $directors = array();
+
+    /**
+     * @var array
+     */
+    public $guestStars = array();
+
+    /**
+     * @var array
+     */
+    public $writers = array();
+
+    /**
+     * @var string
+     */
+    public $name = '';
+
+    /**
+     * @var \DateTime|null
+     */
+    public $firstAired;
+
+    /**
+     * @var string
+     */
+    public $imdbId = '';
+
+    /**
+     * @var string
+     */
+    public $language = Client::DEFAULT_LANGUAGE;
+
+    /**
+     * @var string
+     */
+    public $overview = '';
+
+    /**
+     * @var float
+     */
+    public $rating = '';
+
+    /**
+     * @var int
+     */
+    public $ratingCount = 0;
+
+    /**
+     * @var \DateTime
+     */
+    public $lastUpdated;
+
+    /**
+     * @var int
+     */
+    public $seasonId = 0;
+
+    /**
+     * @var int
+     */
+    public $serieId = 0;
+
+    /**
+     * @var string
+     */
+    public $thumbnail = '';
+
+    /**
+     * Constructor
+     *
+     * @access public
+     * @return \Moinax\TvDb\Episode
+     * @param \SimpleXMLElement $data simplexml element created from thetvdb.com's xml data for the tv episode
+     */
+    public function __construct($data)
+    {
+        $this->id = (int)$data->id;
+        $this->number = (int)$data->EpisodeNumber;
+        $this->season = (int)$data->SeasonNumber;
+        $this->directors = (array)Client::removeEmptyIndexes(explode('|', (string)$data->Director));
+        $this->name = (string)$data->EpisodeName;
+        $this->firstAired = (string)$data->FirstAired !== '' ? new \DateTime((string)$data->FirstAired) : null;
+        $this->guestStars = Client::removeEmptyIndexes(explode('|', (string)$data->GuestStars));
+        $this->imdbId = (string)$data->IMDB_ID;
+        $this->language = (string) $data->Language;
+        $this->overview = (string)$data->Overview;
+        $this->rating = (float)$data->Rating;
+        $this->ratingCount = (int)$data->RatingCount;
+        $this->lastUpdated = \DateTime::createFromFormat('U', (int)$data->lastupdated);
+        $this->writers = (array)Client::removeEmptyIndexes(explode('|', (string)$data->Writer));
+        $this->thumbnail = (string)$data->filename;
+        $this->seasonId = (int)$data->seasonid;
+        $this->serieId = (int)$data->seriesid;
+    }
+}

+ 9 - 0
web/includes/tvdb/Exception.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Moinax\TvDb;
+
+
+class Exception extends \Exception
+{
+
+}

+ 14 - 0
web/includes/tvdb/Http/Cache/Cache.php

@@ -0,0 +1,14 @@
+<?php
+namespace Moinax\TvDb\Http\Cache;
+
+interface Cache
+{
+
+    public function getDate($resource);
+
+    public function setDate($resource, $date);
+
+    public function cache($resource, $date, $content);
+
+    public function getContent($resource);
+}

+ 77 - 0
web/includes/tvdb/Http/Cache/FilesystemCache.php

@@ -0,0 +1,77 @@
+<?php
+namespace Moinax\TvDb\Http\Cache;
+
+class FilesystemCache implements Cache
+{
+
+    private $directory;
+
+    public function __construct($directory)
+    {
+        $this->directory = $directory;
+    }
+
+    /**
+     *
+     * @see \Moinax\TvDb\Http\Cache\Cache::getDate()
+     */
+    public function getDate($resource)
+    {
+        $path = $this->getPath($resource);
+        if(!file_exists($path)) {
+            return -1;
+        }
+        return filemtime($path);
+    }
+
+    /**
+     *
+     * @see \Moinax\TvDb\Http\Cache\Cache::setDate()
+     */
+    public function setDate($resource, $date)
+    {
+        $path = $this->getPath($resource);
+
+        if(! file_exists($path)) {
+            return;
+        }
+
+        touch($path, $date);
+    }
+
+    /**
+     *
+     * @see \Moinax\TvDb\Http\Cache\Cache::cache()
+     */
+    public function cache($resource, $date, $content)
+    {
+        $path = $this->getPath($resource);
+
+        $dirname = dirname($path);
+        if(!is_dir($dirname)) {
+            mkdir($dirname, 0777, true);
+        }
+
+        file_put_contents($path, $content);
+        $this->setDate($resource, $date);
+    }
+
+    /**
+     *
+     * @see \Moinax\TvDb\Http\Cache\Cache::getContent()
+     */
+    public function getContent($resource)
+    {
+        $path = $this->getPath($resource);
+
+        if(! file_exists($path)) {
+            return '';
+        }
+
+        return file_get_contents($path);
+    }
+
+    private function getPath($resource) {
+        return $this->directory . $resource;
+    }
+}

+ 202 - 0
web/includes/tvdb/Http/CacheClient.php

@@ -0,0 +1,202 @@
+<?php
+namespace Moinax\TvDb\Http;
+
+use Moinax\TvDb\CurlException;
+use Moinax\TvDb\Http\Cache\Cache;
+
+class CacheClient implements HttpClient
+{
+
+    /**
+     * Cache to store resources.
+     *
+     * @var Cache
+     */
+    private $cache;
+
+    /**
+     * Minimum resource age before doing any request.
+     *
+     * @var integer
+     */
+    private $ttl;
+
+    /**
+     * Create a new HttpClient with caching features.
+     *
+     * @param Cache $cache
+     * @param integer $ttl
+     */
+    public function __construct(Cache $cache, $ttl)
+    {
+        $this->cache = $cache;
+
+        $this->ttl = $ttl;
+    }
+
+    /**
+     *
+     * @see \Moinax\TvDb\Http\HttpClient::fetch()
+     */
+    public function fetch($url, array $params = array(), $method = HttpClient::GET)
+    {
+        $ch = $this->curlInit($url, $params, $method);
+        if($method == HttpClient::POST) {
+            return $this->doPost($ch, $url);
+        }
+        return $this->doGet($ch, $url);
+    }
+
+    /**
+     * Get the cache time to live.
+     *
+     * @return number
+     */
+    public function getTtl()
+    {
+        return $this->ttl;
+    }
+
+    /**
+     * Set the cache time to live.
+     *
+     * @param integer $ttl
+     */
+    public function setTtl($ttl)
+    {
+        $this->ttl = $ttl;
+    }
+
+    /**
+     * Do a GET request.
+     *
+     * @param resource $ch
+     * @param string $url
+     * @throws CurlException
+     * @return string
+     */
+    private function doGet($ch, $url)
+    {
+        $now = time();
+        $resource = $this->getResourceName($url);
+        $date = $this->cache->getDate($resource);
+        $limit = $now - $this->ttl;
+
+        //Return content if ttl has not yet expired.
+        if($date > 0 && $date > $limit) {
+            return $this->cache->getContent($resource);
+        }
+
+        //Add the If-Modified-Since header
+        if($date > 0) {
+            curl_setopt($ch, CURLOPT_HTTPHEADER, array(
+                sprintf("If-Modified-Since: %s GMT", gmdate("D, d M Y H:i:s", $date)),
+            ));
+        }
+
+        curl_setopt($ch, CURLOPT_FILETIME, true);
+
+        $data = $this->curlExec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $date = curl_getinfo($ch, CURLINFO_FILETIME);
+        curl_close($ch);
+
+        //Return content from cache.
+        if($httpCode == 304) {
+            $this->cache->setDate($resource, $date);
+            return $this->cache->getContent($resource);
+        }
+
+        //Cache content
+        if ($httpCode == 200) {
+            $date = ($date >= 0) ? $date : $now;
+            $this->cache->cache($resource, $date, $data);
+
+            return $data;
+        }
+
+        throw new CurlException(sprintf('Cannot fetch %s', $url), $httpCode);
+    }
+
+    /**
+     * Do a POST request.
+     *
+     * @param resource $ch
+     * @param string $url
+     * @throws CurlException
+     * @return string
+     */
+    private function doPost($ch, $url)
+    {
+        $data = $this->curlExec($ch);
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        curl_close($ch);
+
+        if ($httpCode != 200) {
+            throw new CurlException(sprintf('Cannot fetch %s', $url), $httpCode);
+        }
+
+        return $data;
+    }
+
+    /**
+     * Exec the given cURL connection and return the content body.
+     * @param resource $ch
+     * @return string
+     */
+    private function curlExec($ch)
+    {
+        $response = curl_exec($ch);
+        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+        return substr($response, $headerSize);
+    }
+
+    /**
+     * Initialize a new cURL client.
+     *
+     * @param string $url
+     * @param array  $params
+     * @param string $method
+     *
+     * @return resource
+     */
+    private function curlInit($url, array $params, $method)
+    {
+        $ch = curl_init($url);
+        curl_setopt($ch, CURLOPT_HEADER, 1);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+
+        if ($method == HttpClient::POST) {
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+        }
+
+        return $ch;
+    }
+
+    /**
+     * Get the resource name from url removing the host part and the API key if provided.
+     *
+     * @param string $url
+     * @return string
+     */
+    private function getResourceName($url)
+    {
+        $url = parse_url($url);
+        $extension = pathinfo($url['path'], PATHINFO_EXTENSION);
+
+        $resource = $url['path'];
+        if(in_array($extension, array('xml', 'zip'))) {
+            //Remove the api key from the resource name.
+            $path = explode('/', $url['path']);
+            unset($path[2]);
+            $resource = implode('/', $path);
+        }
+
+        if (!empty($url['query'])) {
+            $resource .= '?' . $url['query'];
+        }
+
+        return $resource;
+    }
+}

+ 38 - 0
web/includes/tvdb/Http/CurlClient.php

@@ -0,0 +1,38 @@
+<?php
+namespace Moinax\TvDb\Http;
+
+use Moinax\TvDb\CurlException;
+
+class CurlClient implements HttpClient
+{
+
+    /**
+     * Fetch the given url contents using cURL library
+     *
+     * @see \Moinax\TvDb\Http\HttpClient::fetch()
+     */
+    public function fetch($url, array $params = array(), $method = HttpClient::GET)
+    {
+        $ch = curl_init($url);
+        curl_setopt($ch, CURLOPT_HEADER, 1);
+        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
+
+        if ($method == HttpClient::POST) {
+            curl_setopt($ch, CURLOPT_POST, 1);
+            curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
+        }
+
+        $response = curl_exec($ch);
+
+        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+        $headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
+        $data = substr($response, $headerSize);
+        curl_close($ch);
+
+        if ($httpCode != 200) {
+            throw new CurlException(sprintf('Cannot fetch %s', $url), $httpCode);
+        }
+
+        return $data;
+    }
+}

+ 12 - 0
web/includes/tvdb/Http/HttpClient.php

@@ -0,0 +1,12 @@
+<?php
+namespace Moinax\TvDb\Http;
+
+interface HttpClient
+{
+
+    const POST = 'post';
+
+    const GET = 'get';
+
+    public function fetch($url, array $params = array(), $method = self::GET);
+}

+ 172 - 0
web/includes/tvdb/Serie.php

@@ -0,0 +1,172 @@
+<?php
+
+namespace Moinax\TvDb;
+
+/**
+ * Serie object
+ *
+ * @package TvDb
+ * @author Jérôme Poskin <moinax@gmail.com>
+ */
+class Serie
+{
+    /**
+     * @var int
+     */
+    public $id;
+
+    /**
+     * @var string
+     */
+    public $language;
+
+    /**
+     * @var string
+     */
+    public $name;
+
+    /**
+     * @var string
+     */
+    public $banner;
+
+    /**
+     * @var string
+     */
+    public $overview;
+
+    /**
+     * @var \DateTime|null
+     */
+    public $firstAired;
+
+    /**
+     * @var string
+     */
+    public $imdbId;
+
+    /**
+     * @var array
+     */
+    public $actors = array();
+
+    /**
+     * @var string
+     */
+    public $airsDayOfWeek = '';
+
+    /**
+     * @var string
+     */
+    public $airsTime = '';
+
+    /**
+     * @var string
+     */
+    public $contentRating = '';
+
+    /**
+     * @var array
+     */
+    public $genres = array();
+
+    /**
+     * @var string
+     */
+    public $network = '';
+
+    /**
+     * @var float
+     */
+    public $rating = '';
+
+    /**
+     * @var int
+     */
+    public $ratingCount = 0;
+
+    /**
+     * @var int
+     */
+    public $runtime = 0;
+
+    /**
+     * @var string
+     */
+    public $status = '';
+
+    /**
+     * @var \DateTime
+     */
+    public $added;
+
+    /**
+     * @var int
+     */
+    public $addedBy;
+
+    /**
+     * @var string
+     */
+    public $fanArt = '';
+
+    /**
+     * @var \DateTime
+     */
+    public $lastUpdated;
+
+    /**
+     * @var string
+     */
+    public $poster = '';
+
+    /**
+     * @var string
+     */
+    public $zap2ItId = '';
+
+    /**
+     * @var array
+     */
+    public $aliasNames = array();
+
+    /**
+     * Constructor
+     *
+     * @access public
+     * @param \SimpleXMLElement $data A simplexml element created from thetvdb.com's xml data for the tv show
+     * @return \Moinax\TvDb\Serie
+     */
+    public function __construct($data)
+    {
+        $this->id = (int)$data->id;
+        $this->language = (string)$data->Language;
+        if(isset($data->language)) {
+            $this->language = (string)$data->language;
+        }
+        $this->name = (string)$data->SeriesName;
+        $this->banner = (string)$data->banner;
+        $this->overview = (string)$data->Overview;
+        $this->firstAired = (string)$data->FirstAired !== '' ? new \DateTime((string)$data->FirstAired) : null;
+        $this->imdbId = (string)$data->IMDB_ID;
+        $this->actors = (array)Client::removeEmptyIndexes(explode('|', (string)$data->Actors));
+        $this->airsDayOfWeek = (string)$data->Airs_DayOfWeek;
+        $this->airsTime = (string)$data->Airs_Time;
+        $this->contentRating = (string)$data->ContentRating;
+        $this->genres = (array)Client::removeEmptyIndexes(explode('|', (string)$data->Genre));
+        $this->network = (string)$data->Network;
+        $this->rating = (float)$data->Rating;
+        $this->ratingCount = (int)$data->RatingCount;
+        $this->runtime = (int)$data->Runtime;
+        $this->status = (string)$data->Status;
+        $this->added = new \DateTime((string)$data->added);
+        $this->addedBy = (int)$data->addedBy;
+        $this->fanArt = (string)$data->fanart;
+        $this->lastUpdated = \DateTime::createFromFormat('U', (int)$data->lastupdated);
+        $this->poster = (string)$data->poster;
+        $this->zap2ItId = (string)$data->zap2it_id;
+        if(isset($data->AliasNames)) {
+            $this->aliasNames = (array)Client::removeEmptyIndexes(explode('|', (string)$data->AliasNames));
+        }
+    }
+}

+ 9 - 0
web/includes/tvdb/XmlException.php

@@ -0,0 +1,9 @@
+<?php
+
+namespace Moinax\TvDb;
+
+
+class XmlException extends Exception
+{
+
+}

+ 9 - 0
web/includes/tvdb/init.php

@@ -0,0 +1,9 @@
+<?php
+	require_once(__DIR__ . '/Http/HttpClient.php');
+	require_once(__DIR__ . '/Http/CurlClient.php');
+	require_once(__DIR__ . '/CurlException.php');
+	require_once(__DIR__ . '/Client.php');
+	require_once(__DIR__ . '/Serie.php');
+	require_once(__DIR__ . '/Banner.php');
+	require_once(__DIR__ . '/Episode.php');
+?>

+ 97 - 0
web/tvdb.php

@@ -0,0 +1,97 @@
+#!/usr/local/bin/php
+<?php
+require_once('includes/main.php');
+require_once('includes/tvdb/init.php');
+
+# TEST
+if (!update(275557)) {
+	return -1;
+}
+return 0;
+# END TEST
+
+function update($id) {
+	return (updateSeries($id) && updateEpisodes($id));
+}
+
+function updateEpisodes($id) {
+
+	# Fetch
+	$tvdb = new Moinax\TvDb\Client(TVDB_URL, TVDB_API_KEY);
+	$episodes = $tvdb->getSerieEpisodes($id);
+
+	# DB init
+	$dbh = new PDO(TVDB_DBN);
+	$insert = $dbh->prepare(
+		'INSERT INTO episodes ("id", "season", "episode", "airdate", "name", "desc", "modified") ' .
+		'VALUES (:id, :season, :number, :airdate, :name, :desc, now())'
+	);
+	$update = $dbh->prepare(
+		'UPDATE episodes SET ' .
+		'airdate=:airdate, name=:name, "desc"=:desc, modified=now() ' .
+		'WHERE id=:id and season=:season and episode=:number'
+	);
+
+	# Update
+	$error = 0;
+	foreach ($episodes['episodes'] as $episode) {
+		$data = array(
+			'id' => $id,
+			'season' => $episode->season,
+			'number' => $episode->number,
+			'airdate' => $episode->firstAired->format('c'), 
+			'name' => $episode->name,
+			'desc' => $episode->overview
+		);
+		if (!insertOrUpdate($insert, $update, $data)){
+			if (DEBUG) {
+				print_r($dbh->errorInfo());
+			}
+			$err++;
+		}
+	}
+	return ($err == 0);
+}
+
+function updateSeries($id) {
+
+	# Fetch series and episodes
+	$tvdb = new Moinax\TvDb\Client(TVDB_URL, TVDB_API_KEY);
+	$series = $tvdb->getSerie($id);
+
+	# DB init
+	$dbh = new PDO(TVDB_DBN);
+	$insert = $dbh->prepare(
+		'INSERT INTO series ("id", "name", "desc", "year", "modified") ' .
+		'VALUES (:id, :name, :desc, :year, now())'
+	);
+	$update = $dbh->prepare(
+		'UPDATE series SET ' .
+		'name=:name, "desc"=:desc, year=:year, modified=now()' .
+		'WHERE id=:id'
+	);
+
+
+	# Update
+	$data = array(
+		'id' => $id,
+		'name' => $series->name,
+		'desc' => $series->overview,
+		'year' => $series->firstAired->format('Y')
+	);
+	if (!insertOrUpdate($insert, $update, $data)){
+		if (DEBUG) {
+			print_r($dbh->errorInfo());
+		}
+		return false;
+	}
+	return true;
+}
+
+function insertOrUpdate($insert, $update, $data) {
+	if (!$insert->execute($data) && !$update->execute($data)) {
+		return false;
+	}
+	return true;
+}
+?>