Merge pull request #539 from JackDandy/feature_UpdateTVDB

Update TvDB API library 1.09 with changes up to (35732c9) with some p…
This commit is contained in:
JackDandy 2015-10-17 22:12:27 +01:00
commit 624485d9a6
10 changed files with 197 additions and 2726 deletions

View file

@ -25,6 +25,7 @@
* Update Tornado Web Server 4.2 to 4.3.dev1 (1b6157d) * Update Tornado Web Server 4.2 to 4.3.dev1 (1b6157d)
* Update change to suppress reporting of Tornado exception error 1 to updated package (ref:hacks.txt) * Update change to suppress reporting of Tornado exception error 1 to updated package (ref:hacks.txt)
* Update fix for API response header for JSON content type and the return of JSONP data to updated package (ref:hacks.txt) * Update fix for API response header for JSON content type and the return of JSONP data to updated package (ref:hacks.txt)
* Update TvDB API library 1.09 with changes up to (35732c9) and some pep8 and code cleanups
* Fix post processing season pack folders * Fix post processing season pack folders
* Fix saving torrent provider option "Seed until ratio" after recent refactor * Fix saving torrent provider option "Seed until ratio" after recent refactor
* Change white text in light theme on Manage / Episode Status Management page to black for better readability * Change white text in light theme on Manage / Episode Status Management page to black for better readability

View file

@ -7,3 +7,4 @@ Libs with customisations...
/lib/requests/packages/urllib3/util/ssl_.py /lib/requests/packages/urllib3/util/ssl_.py
/tornado /tornado
/lib/unrar2/unix.py /lib/unrar2/unix.py
/lib/tvdb/tvdb_api.py

View file

@ -1,4 +0,0 @@
include UNLICENSE
include readme.md
include tests/*.py
include Rakefile

View file

@ -1,103 +0,0 @@
require 'fileutils'
task :default => [:clean]
task :clean do
[".", "tests"].each do |cd|
puts "Cleaning directory #{cd}"
Dir.new(cd).each do |t|
if t =~ /.*\.pyc$/
puts "Removing #{File.join(cd, t)}"
File.delete(File.join(cd, t))
end
end
end
end
desc "Upversion files"
task :upversion do
puts "Upversioning"
Dir.glob("*.py").each do |filename|
f = File.new(filename, File::RDWR)
contents = f.read()
contents.gsub!(/__version__ = ".+?"/){|m|
cur_version = m.scan(/\d+\.\d+/)[0].to_f
new_version = cur_version + 0.1
puts "Current version: #{cur_version}"
puts "New version: #{new_version}"
new_line = "__version__ = \"#{new_version}\""
puts "Old line: #{m}"
puts "New line: #{new_line}"
m = new_line
}
puts contents[0]
f.truncate(0) # empty the existing file
f.seek(0)
f.write(contents.to_s) # write modified file
f.close()
end
end
desc "Upload current version to PyPi"
task :topypi => :test do
cur_file = File.open("tvdb_api.py").read()
tvdb_api_version = cur_file.scan(/__version__ = "(.*)"/)
tvdb_api_version = tvdb_api_version[0][0].to_f
puts "Build sdist and send tvdb_api v#{tvdb_api_version} to PyPi?"
if $stdin.gets.chomp == "y"
puts "Sending source-dist (sdist) to PyPi"
if system("python setup.py sdist register upload")
puts "tvdb_api uploaded!"
end
else
puts "Cancelled"
end
end
desc "Profile by running unittests"
task :profile do
cd "tests"
puts "Profiling.."
`python -m cProfile -o prof_runtest.prof runtests.py`
puts "Converting prof to dot"
`python gprof2dot.py -o prof_runtest.dot -f pstats prof_runtest.prof`
puts "Generating graph"
`~/Applications/dev/graphviz.app/Contents/macOS/dot -Tpng -o profile.png prof_runtest.dot -Gbgcolor=black`
puts "Cleanup"
rm "prof_runtest.dot"
rm "prof_runtest.prof"
end
task :test do
puts "Nosetest'ing"
if not system("nosetests -v --with-doctest")
raise "Test failed!"
end
puts "Doctesting *.py (excluding setup.py)"
Dir.glob("*.py").select{|e| ! e.match(/setup.py/)}.each do |filename|
if filename =~ /^setup\.py/
skip
end
puts "Doctesting #{filename}"
if not system("python", "-m", "doctest", filename)
raise "Failed doctest"
end
end
puts "Doctesting readme.md"
if not system("python", "-m", "doctest", "readme.md")
raise "Doctest"
end
end

View file

@ -1,109 +0,0 @@
# `tvdb_api`
`tvdb_api` is an easy to use interface to [thetvdb.com][tvdb]
`tvnamer` has moved to a separate repository: [github.com/dbr/tvnamer][tvnamer] - it is a utility which uses `tvdb_api` to rename files from `some.show.s01e03.blah.abc.avi` to `Some Show - [01x03] - The Episode Name.avi` (which works by getting the episode name from `tvdb_api`)
[![Build Status](https://secure.travis-ci.org/dbr/tvdb_api.png?branch=master)](http://travis-ci.org/dbr/tvdb_api)
## To install
You can easily install `tvdb_api` via `easy_install`
easy_install tvdb_api
You may need to use sudo, depending on your setup:
sudo easy_install tvdb_api
The [`tvnamer`][tvnamer] command-line tool can also be installed via `easy_install`, this installs `tvdb_api` as a dependancy:
easy_install tvnamer
## Basic usage
import tvdb_api
t = indexerApi()
episode = t['My Name Is Earl'][1][3] # get season 1, episode 3 of show
print episode['episodename'] # Print episode name
## Advanced usage
Most of the documentation is in docstrings. The examples are tested (using doctest) so will always be up to date and working.
The docstring for `Tvdb.__init__` lists all initialisation arguments, including support for non-English searches, custom "Select Series" interfaces and enabling the retrieval of banners and extended actor information. You can also override the default API key using `apikey`, recommended if you're using `tvdb_api` in a larger script or application
### Exceptions
There are several exceptions you may catch, these can be imported from `tvdb_api`:
- `tvdb_error` - this is raised when there is an error communicating with [thetvdb.com][tvdb] (a network error most commonly)
- `tvdb_userabort` - raised when a user aborts the Select Series dialog (by `ctrl+c`, or entering `q`)
- `tvdb_shownotfound` - raised when `t['show name']` cannot find anything
- `tvdb_seasonnotfound` - raised when the requested series (`t['show name][99]`) does not exist
- `tvdb_episodenotfound` - raised when the requested episode (`t['show name][1][99]`) does not exist.
- `tvdb_attributenotfound` - raised when the requested attribute is not found (`t['show name']['an attribute']`, `t['show name'][1]['an attribute']`, or ``t['show name'][1][1]['an attribute']``)
### Series data
All data exposed by [thetvdb.com][tvdb] is accessible via the `Show` class. A Show is retrieved by doing..
>>> import tvdb_api
>>> t = indexerApi()
>>> show = t['scrubs']
>>> type(show)
<class 'tvdb_api.Show'>
For example, to find out what network Scrubs is aired:
>>> t['scrubs']['network']
u'ABC'
The data is stored in an attribute named `data`, within the Show instance:
>>> t['scrubs'].data.keys()
['networkid', 'rating', 'airs_dayofweek', 'contentrating', 'seriesname', 'id', 'airs_time', 'network', 'fanart', 'lastupdated', 'actors', 'ratingcount', 'status', 'added', 'poster', 'imdb_id', 'genre', 'banner', 'seriesid', 'language', 'zap2it_id', 'addedby', 'tms_wanted', 'firstaired', 'runtime', 'overview']
Although each element is also accessible via `t['scrubs']` for ease-of-use:
>>> t['scrubs']['rating']
u'9.0'
This is the recommended way of retrieving "one-off" data (for example, if you are only interested in "seriesname"). If you wish to iterate over all data, or check if a particular show has a specific piece of data, use the `data` attribute,
>>> 'rating' in t['scrubs'].data
True
### Banners and actors
Since banners and actors are separate XML files, retrieving them by default is undesirable. If you wish to retrieve banners (and other fanart), use the `banners` Tvdb initialisation argument:
>>> from tvdb_api import Tvdb
>>> t = Tvdb(banners = True)
Then access the data using a `Show`'s `_banner` key:
>>> t['scrubs']['_banners'].keys()
['fanart', 'poster', 'series', 'season']
The banner data structure will be improved in future versions.
Extended actor data is accessible similarly:
>>> t = Tvdb(actors = True)
>>> actors = t['scrubs']['_actors']
>>> actors[0]
<Actor "Zach Braff">
>>> actors[0].keys()
['sortorder', 'image', 'role', 'id', 'name']
>>> actors[0]['role']
u'Dr. John Michael "J.D." Dorian'
Remember a simple list of actors is accessible via the default Show data:
>>> t['scrubs']['actors']
u'|Zach Braff|Donald Faison|Sarah Chalke|Christa Miller|Aloma Wright|Robert Maschio|Sam Lloyd|Neil Flynn|Ken Jenkins|Judy Reyes|John C. McGinley|Travis Schuldt|Johnny Kastl|Heather Graham|Michael Mosley|Kerry Bish\xe9|Dave Franco|Eliza Coupe|'
[tvdb]: http://thetvdb.com
[tvnamer]: http://github.com/dbr/tvnamer

View file

@ -1,35 +0,0 @@
from setuptools import setup
setup(
name = 'tvdb_api',
version='1.9',
author='dbr/Ben',
description='Interface to thetvdb.com',
url='http://github.com/dbr/tvdb_api/tree/master',
license='unlicense',
long_description="""\
An easy to use API interface to TheTVDB.com
Basic usage is:
>>> import tvdb_api
>>> t = tvdb_api.Tvdb()
>>> ep = t['My Name Is Earl'][1][22]
>>> ep
<Episode 01x22 - Stole a Badge>
>>> ep['episodename']
u'Stole a Badge'
""",
py_modules = ['tvdb_api', 'tvdb_ui', 'tvdb_exceptions', 'tvdb_cache'],
classifiers=[
"Intended Audience :: Developers",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Multimedia",
"Topic :: Utilities",
"Topic :: Software Development :: Libraries :: Python Modules",
]
)

File diff suppressed because it is too large Load diff

View file

@ -1,28 +0,0 @@
#!/usr/bin/env python2
#encoding:utf-8
#author:dbr/Ben
#project:tvdb_api
#repository:http://github.com/dbr/tvdb_api
#license:unlicense (http://unlicense.org/)
import sys
import unittest
import test_tvdb_api
def main():
suite = unittest.TestSuite([
unittest.TestLoader().loadTestsFromModule(test_tvdb_api)
])
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
if result.wasSuccessful():
return 0
else:
return 1
if __name__ == '__main__':
sys.exit(
int(main())
)

View file

@ -1,577 +0,0 @@
#!/usr/bin/env python2
#encoding:utf-8
#author:dbr/Ben
#project:tvdb_api
#repository:http://github.com/dbr/tvdb_api
#license:unlicense (http://unlicense.org/)
"""Unittests for tvdb_api
"""
import os,os.path
import sys
print sys.path
import datetime
import unittest
# Force parent directory onto path
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
sys.path.insert(1, os.path.abspath('../../tests'))
import sickbeard
from tvdb_api import Tvdb
import tvdb_ui
from tvdb_api import (tvdb_shownotfound, tvdb_seasonnotfound,
tvdb_episodenotfound, tvdb_attributenotfound)
from lib import xmltodict
import lib
class test_tvdb_basic(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
def test_different_case(self):
"""Checks the auto-correction of show names is working.
It should correct the weirdly capitalised 'sCruBs' to 'Scrubs'
"""
self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady')
self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs')
def test_spaces(self):
"""Checks shownames with spaces
"""
self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl')
self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death')
def test_numeric(self):
"""Checks numeric show names
"""
self.assertEquals(self.t['24'][2][20]['episodename'], 'Day 2: 3:00 A.M.-4:00 A.M.')
self.assertEquals(self.t['24']['seriesname'], '24')
def test_show_iter(self):
"""Iterating over a show returns each seasons
"""
self.assertEquals(
len(
[season for season in self.t['Life on Mars']]
),
2
)
def test_season_iter(self):
"""Iterating over a show returns episodes
"""
self.assertEquals(
len(
[episode for episode in self.t['Life on Mars'][1]]
),
8
)
def test_get_episode_overview(self):
"""Checks episode overview is retrieved correctly.
"""
self.assertEquals(
self.t['Battlestar Galactica (2003)'][1][6]['overview'].startswith(
'When a new copy of Doral, a Cylon who had been previously'),
True
)
def test_get_parent(self):
"""Check accessing series from episode instance
"""
show = self.t['Battlestar Galactica (2003)']
season = show[1]
episode = show[1][1]
self.assertEquals(
season.show,
show
)
self.assertEquals(
episode.season,
season
)
self.assertEquals(
episode.season.show,
show
)
def test_no_season(self):
show = self.t['Katekyo Hitman Reborn']
print tvdb_api
print show[1][1]
class test_tvdb_errors(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
def test_seasonnotfound(self):
"""Checks exception is thrown when season doesn't exist.
"""
self.assertRaises(tvdb_seasonnotfound, lambda:self.t['CNNNN'][10][1])
def test_shownotfound(self):
"""Checks exception is thrown when episode doesn't exist.
"""
self.assertRaises(tvdb_shownotfound, lambda:self.t['the fake show thingy'])
def test_episodenotfound(self):
"""Checks exception is raised for non-existent episode
"""
self.assertRaises(tvdb_episodenotfound, lambda:self.t['Scrubs'][1][30])
def test_attributenamenotfound(self):
"""Checks exception is thrown for if an attribute isn't found.
"""
self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN'][1][6]['afakeattributething'])
self.assertRaises(tvdb_attributenotfound, lambda:self.t['CNNNN']['afakeattributething'])
class test_tvdb_search(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
def test_search_len(self):
"""There should be only one result matching
"""
self.assertEquals(len(self.t['My Name Is Earl'].search('Faked His Own Death')), 1)
def test_search_checkname(self):
"""Checks you can get the episode name of a search result
"""
self.assertEquals(self.t['Scrubs'].search('my first')[0]['episodename'], 'My First Day')
self.assertEquals(self.t['My Name Is Earl'].search('Faked His Own Death')[0]['episodename'], 'Faked His Own Death')
def test_search_multiresults(self):
"""Checks search can return multiple results
"""
self.assertEquals(len(self.t['Scrubs'].search('my first')) >= 3, True)
def test_search_no_params_error(self):
"""Checks not supplying search info raises TypeError"""
self.assertRaises(
TypeError,
lambda: self.t['Scrubs'].search()
)
def test_search_season(self):
"""Checks the searching of a single season"""
self.assertEquals(
len(self.t['Scrubs'][1].search("First")),
3
)
def test_search_show(self):
"""Checks the searching of an entire show"""
self.assertEquals(
len(self.t['CNNNN'].search('CNNNN', key='episodename')),
3
)
def test_aired_on(self):
"""Tests airedOn show method"""
sr = self.t['Scrubs'].airedOn(datetime.date(2001, 10, 2))
self.assertEquals(len(sr), 1)
self.assertEquals(sr[0]['episodename'], u'My First Day')
class test_tvdb_data(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
def test_episode_data(self):
"""Check the firstaired value is retrieved
"""
self.assertEquals(
self.t['lost']['firstaired'],
'2004-09-22'
)
class test_tvdb_misc(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
def test_repr_show(self):
"""Check repr() of Season
"""
self.assertEquals(
repr(self.t['CNNNN']),
"<Show Chaser Non-Stop News Network (CNNNN) (containing 3 seasons)>"
)
def test_repr_season(self):
"""Check repr() of Season
"""
self.assertEquals(
repr(self.t['CNNNN'][1]),
"<Season instance (containing 9 episodes)>"
)
def test_repr_episode(self):
"""Check repr() of Episode
"""
self.assertEquals(
repr(self.t['CNNNN'][1][1]),
"<Episode 01x01 - Terror Alert>"
)
def test_have_all_languages(self):
"""Check valid_languages is up-to-date (compared to languages.xml)
"""
et = self.t._getetsrc(
"http://thetvdb.com/api/%s/languages.xml" % (
self.t.config['apikey']
)
)
languages = [x.find("abbreviation").text for x in et.findall("Language")]
self.assertEquals(
sorted(languages),
sorted(self.t.config['valid_languages'])
)
class test_tvdb_languages(unittest.TestCase):
def test_episode_name_french(self):
"""Check episode data is in French (language="fr")
"""
t = tvdb_api.Tvdb(cache = True, language = "fr")
self.assertEquals(
t['scrubs'][1][1]['episodename'],
"Mon premier jour"
)
self.assertTrue(
t['scrubs']['overview'].startswith(
u"J.D. est un jeune m\xe9decin qui d\xe9bute"
)
)
def test_episode_name_spanish(self):
"""Check episode data is in Spanish (language="es")
"""
t = tvdb_api.Tvdb(cache = True, language = "es")
self.assertEquals(
t['scrubs'][1][1]['episodename'],
"Mi Primer Dia"
)
self.assertTrue(
t['scrubs']['overview'].startswith(
u'Scrubs es una divertida comedia'
)
)
def test_multilanguage_selection(self):
"""Check selected language is used
"""
class SelectEnglishUI(tvdb_ui.BaseUI):
def selectSeries(self, allSeries):
return [x for x in allSeries if x['language'] == "en"][0]
class SelectItalianUI(tvdb_ui.BaseUI):
def selectSeries(self, allSeries):
return [x for x in allSeries if x['language'] == "it"][0]
t_en = tvdb_api.Tvdb(
cache=True,
custom_ui = SelectEnglishUI,
language = "en")
t_it = tvdb_api.Tvdb(
cache=True,
custom_ui = SelectItalianUI,
language = "it")
self.assertEquals(
t_en['dexter'][1][2]['episodename'], "Crocodile"
)
self.assertEquals(
t_it['dexter'][1][2]['episodename'], "Lacrime di coccodrillo"
)
class test_tvdb_unicode(unittest.TestCase):
def test_search_in_chinese(self):
"""Check searching for show with language=zh returns Chinese seriesname
"""
t = tvdb_api.Tvdb(cache = True, language = "zh")
show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i']
self.assertEquals(
type(show),
tvdb_api.Show
)
self.assertEquals(
show['seriesname'],
u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i'
)
def test_search_in_all_languages(self):
"""Check search_all_languages returns Chinese show, with language=en
"""
t = tvdb_api.Tvdb(cache = True, search_all_languages = True, language="en")
show = t[u'T\xecnh Ng\u01b0\u1eddi Hi\u1ec7n \u0110\u1ea1i']
self.assertEquals(
type(show),
tvdb_api.Show
)
self.assertEquals(
show['seriesname'],
u'Virtues Of Harmony II'
)
class test_tvdb_banners(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = True)
def test_have_banners(self):
"""Check banners at least one banner is found
"""
self.assertEquals(
len(self.t['scrubs']['_banners']) > 0,
True
)
def test_banner_url(self):
"""Checks banner URLs start with http://
"""
for banner_type, banner_data in self.t['scrubs']['_banners'].items():
for res, res_data in banner_data.items():
for bid, banner_info in res_data.items():
self.assertEquals(
banner_info['_bannerpath'].startswith("http://"),
True
)
def test_episode_image(self):
"""Checks episode 'filename' image is fully qualified URL
"""
self.assertEquals(
self.t['scrubs'][1][1]['filename'].startswith("http://"),
True
)
def test_show_artwork(self):
"""Checks various image URLs within season data are fully qualified
"""
for key in ['banner', 'fanart', 'poster']:
self.assertEquals(
self.t['scrubs'][key].startswith("http://"),
True
)
class test_tvdb_actors(unittest.TestCase):
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True)
def test_actors_is_correct_datatype(self):
"""Check show/_actors key exists and is correct type"""
self.assertTrue(
isinstance(
self.t['scrubs']['_actors'],
tvdb_api.Actors
)
)
def test_actors_has_actor(self):
"""Check show has at least one Actor
"""
self.assertTrue(
isinstance(
self.t['scrubs']['_actors'][0],
tvdb_api.Actor
)
)
def test_actor_has_name(self):
"""Check first actor has a name"""
self.assertEquals(
self.t['scrubs']['_actors'][0]['name'],
"Zach Braff"
)
def test_actor_image_corrected(self):
"""Check image URL is fully qualified
"""
for actor in self.t['scrubs']['_actors']:
if actor['image'] is not None:
# Actor's image can be None, it displays as the placeholder
# image on thetvdb.com
self.assertTrue(
actor['image'].startswith("http://")
)
class test_tvdb_doctest(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, banners = False)
def test_doctest(self):
"""Check docstring examples works"""
import doctest
doctest.testmod(tvdb_api)
class test_tvdb_custom_caching(unittest.TestCase):
def test_true_false_string(self):
"""Tests setting cache to True/False/string
Basic tests, only checking for errors
"""
tvdb_api.Tvdb(cache = True)
tvdb_api.Tvdb(cache = False)
tvdb_api.Tvdb(cache = "/tmp")
def test_invalid_cache_option(self):
"""Tests setting cache to invalid value
"""
try:
tvdb_api.Tvdb(cache = 2.3)
except ValueError:
pass
else:
self.fail("Expected ValueError from setting cache to float")
def test_custom_urlopener(self):
class UsedCustomOpener(Exception):
pass
import urllib2
class TestOpener(urllib2.BaseHandler):
def default_open(self, request):
print request.get_method()
raise UsedCustomOpener("Something")
custom_opener = urllib2.build_opener(TestOpener())
t = tvdb_api.Tvdb(cache = custom_opener)
try:
t['scrubs']
except UsedCustomOpener:
pass
else:
self.fail("Did not use custom opener")
class test_tvdb_by_id(unittest.TestCase):
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True)
def test_actors_is_correct_datatype(self):
"""Check show/_actors key exists and is correct type"""
self.assertEquals(
self.t[76156]['seriesname'],
'Scrubs'
)
class test_tvdb_zip(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True)
def test_get_series_from_zip(self):
"""
"""
self.assertEquals(self.t['scrubs'][1][4]['episodename'], 'My Old Lady')
self.assertEquals(self.t['sCruBs']['seriesname'], 'Scrubs')
def test_spaces_from_zip(self):
"""Checks shownames with spaces
"""
self.assertEquals(self.t['My Name Is Earl']['seriesname'], 'My Name Is Earl')
self.assertEquals(self.t['My Name Is Earl'][1][4]['episodename'], 'Faked His Own Death')
class test_tvdb_show_ordering(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t_dvd = None
t_air = None
def setUp(self):
if self.t_dvd is None:
self.t_dvd = tvdb_api.Tvdb(cache = True, useZip = True, dvdorder=True)
if self.t_air is None:
self.t_air = tvdb_api.Tvdb(cache = True, useZip = True)
def test_ordering(self):
"""Test Tvdb.search method
"""
self.assertEquals(u'The Train Job', self.t_air['Firefly'][1][1]['episodename'])
self.assertEquals(u'Serenity', self.t_dvd['Firefly'][1][1]['episodename'])
self.assertEquals(u'The Cat & the Claw (Part 1)', self.t_air['Batman The Animated Series'][1][1]['episodename'])
self.assertEquals(u'On Leather Wings', self.t_dvd['Batman The Animated Series'][1][1]['episodename'])
class test_tvdb_show_search(unittest.TestCase):
# Used to store the cached instance of Tvdb()
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, useZip = True)
def test_search(self):
"""Test Tvdb.search method
"""
results = self.t.search("my name is earl")
all_ids = [x['seriesid'] for x in results]
self.assertTrue('75397' in all_ids)
class test_tvdb_alt_names(unittest.TestCase):
t = None
def setUp(self):
if self.t is None:
self.__class__.t = tvdb_api.Tvdb(cache = True, actors = True)
def test_1(self):
"""Tests basic access of series name alias
"""
results = self.t.search("Don't Trust the B---- in Apartment 23")
series = results[0]
self.assertTrue(
'Apartment 23' in series['aliasnames']
)
if __name__ == '__main__':
runner = unittest.TextTestRunner(verbosity = 2)
unittest.main(testRunner = runner)

View file

@ -1,18 +1,17 @@
# !/usr/bin/env python2 # !/usr/bin/env python2
#encoding:utf-8 # encoding:utf-8
#author:dbr/Ben # author:dbr/Ben
#project:tvdb_api # project:tvdb_api
#repository:http://github.com/dbr/tvdb_api # repository:http://github.com/dbr/tvdb_api
#license:unlicense (http://unlicense.org/) # license:unlicense (http://unlicense.org/)
from functools import wraps from functools import wraps
import traceback import traceback
__author__ = "dbr/Ben" __author__ = 'dbr/Ben'
__version__ = "1.9" __version__ = '1.9'
import os import os
import re
import time import time
import getpass import getpass
import StringIO import StringIO
@ -20,7 +19,6 @@ import tempfile
import warnings import warnings
import logging import logging
import zipfile import zipfile
import datetime as dt
import requests import requests
import requests.exceptions import requests.exceptions
import xmltodict import xmltodict
@ -39,12 +37,12 @@ from lib.dateutil.parser import parse
from lib.cachecontrol import CacheControl, caches from lib.cachecontrol import CacheControl, caches
from tvdb_ui import BaseUI, ConsoleUI from tvdb_ui import BaseUI, ConsoleUI
from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound, from tvdb_exceptions import (tvdb_error, tvdb_shownotfound,
tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound) tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
def log(): def log():
return logging.getLogger("tvdb_api") return logging.getLogger('tvdb_api')
def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None): def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
@ -76,7 +74,7 @@ def retry(ExceptionToCheck, tries=4, delay=3, backoff=2, logger=None):
try: try:
return f(*args, **kwargs) return f(*args, **kwargs)
except ExceptionToCheck, e: except ExceptionToCheck, e:
msg = "%s, Retrying in %d seconds..." % (str(e), mdelay) msg = '%s, Retrying in %d seconds...' % (str(e), mdelay)
if logger: if logger:
logger.warning(msg) logger.warning(msg)
else: else:
@ -99,10 +97,10 @@ class ShowContainer(dict):
self._stack = [] self._stack = []
self._lastgc = time.time() self._lastgc = time.time()
def __setitem__(self, key, value): def __set_item__(self, key, value):
self._stack.append(key) self._stack.append(key)
#keep only the 100th latest results # keep only the 100th latest results
if time.time() - self._lastgc > 20: if time.time() - self._lastgc > 20:
for o in self._stack[:-100]: for o in self._stack[:-100]:
del self[o] del self[o]
@ -111,7 +109,7 @@ class ShowContainer(dict):
self._lastgc = time.time() self._lastgc = time.time()
super(ShowContainer, self).__setitem__(key, value) super(ShowContainer, self).__set_item__(key, value)
class Show(dict): class Show(dict):
@ -123,10 +121,7 @@ class Show(dict):
self.data = {} self.data = {}
def __repr__(self): def __repr__(self):
return "<Show %s (containing %s seasons)>" % ( return '<Show %r (containing %s seasons)>' % (self.data.get(u'seriesname', 'instance'), len(self))
self.data.get(u'seriesname', 'instance'),
len(self)
)
def __getattr__(self, key): def __getattr__(self, key):
if key in self: if key in self:
@ -151,16 +146,16 @@ class Show(dict):
# Data wasn't found, raise appropriate error # Data wasn't found, raise appropriate error
if isinstance(key, int) or key.isdigit(): if isinstance(key, int) or key.isdigit():
# Episode number x was not found # Episode number x was not found
raise tvdb_seasonnotfound("Could not find season %s" % (repr(key))) raise tvdb_seasonnotfound('Could not find season %s' % (repr(key)))
else: else:
# If it's not numeric, it must be an attribute name, which # If it's not numeric, it must be an attribute name, which
# doesn't exist, so attribute error. # doesn't exist, so attribute error.
raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) raise tvdb_attributenotfound('Cannot find attribute %s' % (repr(key)))
def airedOn(self, date): def airedOn(self, date):
ret = self.search(str(date), 'firstaired') ret = self.search(str(date), 'firstaired')
if len(ret) == 0: if 0 == len(ret):
raise tvdb_episodenotfound("Could not find any episodes that aired on %s" % date) raise tvdb_episodenotfound('Could not find any episodes that aired on %s' % date)
return ret return ret
def search(self, term=None, key=None): def search(self, term=None, key=None):
@ -181,43 +176,43 @@ class Show(dict):
These examples assume t is an instance of Tvdb(): These examples assume t is an instance of Tvdb():
>>> t = Tvdb() >> t = Tvdb()
>>> >>
To search for all episodes of Scrubs with a bit of data To search for all episodes of Scrubs with a bit of data
containing "my first day": containing "my first day":
>>> t['Scrubs'].search("my first day") >> t['Scrubs'].search("my first day")
[<Episode 01x01 - My First Day>] [<Episode 01x01 - My First Day>]
>>> >>
Search for "My Name Is Earl" episode named "Faked His Own Death": Search for "My Name Is Earl" episode named "Faked His Own Death":
>>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename') >> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
[<Episode 01x04 - Faked His Own Death>] [<Episode 01x04 - Faked His Own Death>]
>>> >>
To search Scrubs for all episodes with "mentor" in the episode name: To search Scrubs for all episodes with "mentor" in the episode name:
>>> t['scrubs'].search('mentor', key = 'episodename') >> t['scrubs'].search('mentor', key = 'episodename')
[<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>] [<Episode 01x02 - My Mentor>, <Episode 03x15 - My Tormented Mentor>]
>>> >>
# Using search results # Using search results
>>> results = t['Scrubs'].search("my first") >> results = t['Scrubs'].search("my first")
>>> print results[0]['episodename'] >> print results[0]['episodename']
My First Day My First Day
>>> for x in results: print x['episodename'] >> for x in results: print x['episodename']
My First Day My First Day
My First Step My First Step
My First Kill My First Kill
>>> >>
""" """
results = [] results = []
for cur_season in self.values(): for cur_season in self.values():
searchresult = cur_season.search(term=term, key=key) searchresult = cur_season.search(term=term, key=key)
if len(searchresult) != 0: if 0 != len(searchresult):
results.extend(searchresult) results.extend(searchresult)
return results return results
@ -230,9 +225,7 @@ class Season(dict):
self.show = show self.show = show
def __repr__(self): def __repr__(self):
return "<Season instance (containing %s episodes)>" % ( return '<Season instance (containing %s episodes)>' % (len(self.keys()))
len(self.keys())
)
def __getattr__(self, episode_number): def __getattr__(self, episode_number):
if episode_number in self: if episode_number in self:
@ -241,7 +234,7 @@ class Season(dict):
def __getitem__(self, episode_number): def __getitem__(self, episode_number):
if episode_number not in self: if episode_number not in self:
raise tvdb_episodenotfound("Could not find episode %s" % (repr(episode_number))) raise tvdb_episodenotfound('Could not find episode %s' % (repr(episode_number)))
else: else:
return dict.__getitem__(self, episode_number) return dict.__getitem__(self, episode_number)
@ -249,20 +242,18 @@ class Season(dict):
"""Search all episodes in season, returns a list of matching Episode """Search all episodes in season, returns a list of matching Episode
instances. instances.
>>> t = Tvdb() >> t = Tvdb()
>>> t['scrubs'][1].search('first day') >> t['scrubs'][1].search('first day')
[<Episode 01x01 - My First Day>] [<Episode 01x01 - My First Day>]
>>> >>
See Show.search documentation for further information on search See Show.search documentation for further information on search
""" """
results = [] results = []
for ep in self.values(): for ep in self.values():
searchresult = ep.search(term=term, key=key) searchresult = ep.search(term=term, key=key)
if searchresult is not None: if None is not searchresult:
results.append( results.append(searchresult)
searchresult
)
return results return results
@ -273,13 +264,12 @@ class Episode(dict):
self.season = season self.season = season
def __repr__(self): def __repr__(self):
seasno = int(self.get(u'seasonnumber', 0)) seasno, epno = int(self.get(u'seasonnumber', 0)), int(self.get(u'episodenumber', 0))
epno = int(self.get(u'episodenumber', 0))
epname = self.get(u'episodename') epname = self.get(u'episodename')
if epname is not None: if None is not epname:
return "<Episode %02dx%02d - %s>" % (seasno, epno, epname) return '<Episode %02dx%02d - %r>' % (seasno, epno, epname)
else: else:
return "<Episode %02dx%02d>" % (seasno, epno) return '<Episode %02dx%02d>' % (seasno, epno)
def __getattr__(self, key): def __getattr__(self, key):
if key in self: if key in self:
@ -290,7 +280,7 @@ class Episode(dict):
try: try:
return dict.__getitem__(self, key) return dict.__getitem__(self, key)
except KeyError: except KeyError:
raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key))) raise tvdb_attributenotfound('Cannot find attribute %s' % (repr(key)))
def search(self, term=None, key=None): def search(self, term=None, key=None):
"""Search episode data for term, if it matches, return the Episode (self). """Search episode data for term, if it matches, return the Episode (self).
@ -302,25 +292,25 @@ class Episode(dict):
Simple example: Simple example:
>>> e = Episode() >> e = Episode()
>>> e['episodename'] = "An Example" >> e['episodename'] = "An Example"
>>> e.search("examp") >> e.search("examp")
<Episode 00x00 - An Example> <Episode 00x00 - An Example>
>>> >>
Limiting by key: Limiting by key:
>>> e.search("examp", key = "episodename") >> e.search("examp", key = "episodename")
<Episode 00x00 - An Example> <Episode 00x00 - An Example>
>>> >>
""" """
if term == None: if None is term:
raise TypeError("must supply string to search for (contents)") raise TypeError('must supply string to search for (contents)')
term = unicode(term).lower() term = unicode(term).lower()
for cur_key, cur_value in self.items(): for cur_key, cur_value in self.items():
cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower() cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
if key is not None and cur_key != key: if None is not key and cur_key != key:
# Do not search this key # Do not search this key
continue continue
if cur_value.find(unicode(term).lower()) > -1: if cur_value.find(unicode(term).lower()) > -1:
@ -344,13 +334,13 @@ class Actor(dict):
""" """
def __repr__(self): def __repr__(self):
return "<Actor \"%s\">" % (self.get("name")) return '<Actor "%r">' % self.get('name')
class Tvdb: class Tvdb:
"""Create easy-to-use interface to name of season/episode name """Create easy-to-use interface to name of season/episode name
>>> t = Tvdb() >> t = Tvdb()
>>> t['Scrubs'][1][24]['episodename'] >> t['Scrubs'][1][24]['episodename']
u'My Last Day' u'My Last Day'
""" """
@ -382,8 +372,8 @@ class Tvdb:
debug (True/False) DEPRECATED: debug (True/False) DEPRECATED:
Replaced with proper use of logging module. To show debug messages: Replaced with proper use of logging module. To show debug messages:
>>> import logging >> import logging
>>> logging.basicConfig(level = logging.DEBUG) >> logging.basicConfig(level = logging.DEBUG)
cache (True/False/str/unicode/urllib2 opener): cache (True/False/str/unicode/urllib2 opener):
Retrieved XML are persisted to to disc. If true, stores in Retrieved XML are persisted to to disc. If true, stores in
@ -397,15 +387,15 @@ class Tvdb:
Retrieves the banners for a show. These are accessed Retrieves the banners for a show. These are accessed
via the _banners key of a Show(), for example: via the _banners key of a Show(), for example:
>>> Tvdb(banners=True)['scrubs']['_banners'].keys() >> Tvdb(banners=True)['scrubs']['_banners'].keys()
['fanart', 'poster', 'series', 'season'] ['fanart', 'poster', 'series', 'season']
actors (True/False): actors (True/False):
Retrieves a list of the actors for a show. These are accessed Retrieves a list of the actors for a show. These are accessed
via the _actors key of a Show(), for example: via the _actors key of a Show(), for example:
>>> t = Tvdb(actors=True) >> t = Tvdb(actors=True)
>>> t['scrubs']['_actors'][0]['name'] >> t['scrubs']['_actors'][0]['name']
u'Zach Braff' u'Zach Braff'
custom_ui (tvdb_ui.BaseUI subclass): custom_ui (tvdb_ui.BaseUI subclass):
@ -415,7 +405,7 @@ class Tvdb:
The language of the returned data. Is also the language search The language of the returned data. Is also the language search
uses. Default is "en" (English). For full list, run.. uses. Default is "en" (English). For full list, run..
>>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS >> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS
['da', 'fi', 'nl', ...] ['da', 'fi', 'nl', ...]
search_all_languages (True/False): search_all_languages (True/False):
@ -447,10 +437,10 @@ class Tvdb:
self.config = {} self.config = {}
if apikey is not None: if None is not apikey:
self.config['apikey'] = apikey self.config['apikey'] = apikey
else: else:
self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key self.config['apikey'] = '0629B785CE550C8D' # tvdb_api's API key
self.config['debug_enabled'] = debug # show debugging messages self.config['debug_enabled'] = debug # show debugging messages
@ -470,31 +460,30 @@ class Tvdb:
if cache is True: if cache is True:
self.config['cache_enabled'] = True self.config['cache_enabled'] = True
self.config['cache_location'] = self._getTempDir() self.config['cache_location'] = self._get_temp_dir()
elif cache is False: elif cache is False:
self.config['cache_enabled'] = False self.config['cache_enabled'] = False
elif isinstance(cache, basestring): elif isinstance(cache, basestring):
self.config['cache_enabled'] = True self.config['cache_enabled'] = True
self.config['cache_location'] = cache self.config['cache_location'] = cache
else: else:
raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache))) raise ValueError('Invalid value for Cache %r (type was %s)' % (cache, type(cache)))
self.config['banners_enabled'] = banners self.config['banners_enabled'] = banners
self.config['actors_enabled'] = actors self.config['actors_enabled'] = actors
if self.config['debug_enabled']: if self.config['debug_enabled']:
warnings.warn("The debug argument to tvdb_api.__init__ will be removed in the next version. " warnings.warn('The debug argument to tvdb_api.__init__ will be removed in the next version. ' +
"To enable debug messages, use the following code before importing: " 'To enable debug messages, use the following code before importing: ' +
"import logging; logging.basicConfig(level=logging.DEBUG)") 'import logging; logging.basicConfig(level=logging.DEBUG)')
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
# List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml # List of language from http://thetvdb.com/api/0629B785CE550C8D/languages.xml
# Hard-coded here as it is realtively static, and saves another HTTP request, as # Hard-coded here as it is realtively static, and saves another HTTP request, as
# recommended on http://thetvdb.com/wiki/index.php/API:languages.xml # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
self.config['valid_languages'] = [ self.config['valid_languages'] = [
"da", "fi", "nl", "de", "it", "es", "fr", "pl", "hu", "el", "tr", 'da', 'fi', 'nl', 'de', 'it', 'es', 'fr', 'pl', 'hu', 'el', 'tr',
"ru", "he", "ja", "pt", "zh", "cs", "sl", "hr", "ko", "en", "sv", "no" 'ru', 'he', 'ja', 'pt', 'zh', 'cs', 'sl', 'hr', 'ko', 'en', 'sv', 'no'
] ]
# thetvdb.com should be based around numeric language codes, # thetvdb.com should be based around numeric language codes,
@ -506,58 +495,52 @@ class Tvdb:
'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11, 'tr': 21, 'pl': 18, 'fr': 17, 'hr': 31, 'de': 14, 'da': 10, 'fi': 11,
'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30} 'hu': 19, 'ja': 25, 'he': 24, 'ko': 32, 'sv': 8, 'sl': 30}
if language is None: if None is language:
self.config['language'] = 'en' self.config['language'] = 'en'
else: else:
if language not in self.config['valid_languages']: if language not in self.config['valid_languages']:
raise ValueError("Invalid language %s, options are: %s" % ( raise ValueError('Invalid language %s, options are: %s' % (language, self.config['valid_languages']))
language, self.config['valid_languages']
))
else: else:
self.config['language'] = language self.config['language'] = language
# The following url_ configs are based of the # The following url_ configs are based of the
# http://thetvdb.com/wiki/index.php/Programmers_API # http://thetvdb.com/wiki/index.php/Programmers_API
self.config['base_url'] = "http://thetvdb.com" self.config['base_url'] = 'http://thetvdb.com'
if self.config['search_all_languages']: if self.config['search_all_languages']:
self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php" % self.config self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config
self.config['params_getSeries'] = {"seriesname": "", "language": "all"} self.config['params_get_series'] = {'seriesname': '', 'language': 'all'}
else: else:
self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php" % self.config self.config['url_get_series'] = u'%(base_url)s/api/GetSeries.php' % self.config
self.config['params_getSeries'] = {"seriesname": "", "language": self.config['language']} self.config['params_get_series'] = {'seriesname': '', 'language': self.config['language']}
self.config['url_epInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml" % self.config self.config['url_epInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.xml' % self.config
self.config['url_epInfo_zip'] = u"%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip" % self.config self.config['url_epInfo_zip'] = u'%(base_url)s/api/%(apikey)s/series/%%s/all/%%s.zip' % self.config
self.config['url_seriesInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml" % self.config self.config['url_seriesInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/%%s.xml' % self.config
self.config['url_actorsInfo'] = u"%(base_url)s/api/%(apikey)s/series/%%s/actors.xml" % self.config self.config['url_actorsInfo'] = u'%(base_url)s/api/%(apikey)s/series/%%s/actors.xml' % self.config
self.config['url_seriesBanner'] = u"%(base_url)s/api/%(apikey)s/series/%%s/banners.xml" % self.config self.config['url_seriesBanner'] = u'%(base_url)s/api/%(apikey)s/series/%%s/banners.xml' % self.config
self.config['url_artworkPrefix'] = u"%(base_url)s/banners/%%s" % self.config self.config['url_artworkPrefix'] = u'%(base_url)s/banners/%%s' % self.config
self.config['url_updates_all'] = u"%(base_url)s/api/%(apikey)s/updates_all.zip" % self.config @staticmethod
self.config['url_updates_month'] = u"%(base_url)s/api/%(apikey)s/updates_month.zip" % self.config def _get_temp_dir():
self.config['url_updates_week'] = u"%(base_url)s/api/%(apikey)s/updates_week.zip" % self.config
self.config['url_updates_day'] = u"%(base_url)s/api/%(apikey)s/updates_day.zip" % self.config
def _getTempDir(self):
"""Returns the [system temp dir]/tvdb_api-u501 (or """Returns the [system temp dir]/tvdb_api-u501 (or
tvdb_api-myuser) tvdb_api-myuser)
""" """
if hasattr(os, 'getuid'): if hasattr(os, 'getuid'):
uid = "u%d" % (os.getuid()) uid = 'u%d' % (os.getuid())
else: else:
# For Windows # For Windows
try: try:
uid = getpass.getuser() uid = getpass.getuser()
except ImportError: except ImportError:
return os.path.join(tempfile.gettempdir(), "tvdb_api") return os.path.join(tempfile.gettempdir(), 'tvdb_api')
return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid)) return os.path.join(tempfile.gettempdir(), 'tvdb_api-%s' % uid)
@retry(tvdb_error) @retry(tvdb_error)
def _loadUrl(self, url, params=None, language=None): def _load_url(self, url, params=None, language=None):
log().debug('Retrieving URL %s' % url) log().debug('Retrieving URL %s' % url)
session = requests.session() session = requests.session()
@ -587,21 +570,11 @@ class Tvdb:
# clean up value and do type changes # clean up value and do type changes
if value: if value:
if 'firstaired' == key:
try: try:
if key == 'firstaired' and value in '0000-00-00': value = parse(value, fuzzy=True).strftime('%Y-%m-%d')
new_value = str(dt.date.fromordinal(1))
new_value = re.sub('([-]0{2})+', '', new_value)
fix_date = parse(new_value, fuzzy=True).date()
value = fix_date.strftime('%Y-%m-%d')
elif key == 'firstaired':
value = parse(value, fuzzy=True).date()
value = value.strftime('%Y-%m-%d')
#if key == 'airs_time':
# value = parse(value).time()
# value = value.strftime('%I:%M %p')
except: except:
pass value = None
return key, value return key, value
@ -626,14 +599,14 @@ class Tvdb:
"""Loads a URL using caching, returns an ElementTree of the source """Loads a URL using caching, returns an ElementTree of the source
""" """
try: try:
src = self._loadUrl(url, params=params, language=language).values()[0] src = self._load_url(url, params=params, language=language).values()[0]
return src return src
except: except:
return [] return []
def _setItem(self, sid, seas, ep, attrib, value): def _set_item(self, sid, seas, ep, attrib, value):
"""Creates a new episode, creating Show(), Season() and """Creates a new episode, creating Show(), Season() and
Episode()s as required. Called by _getShowData to populate show Episode()s as required. Called by _get_show_data to populate show
Since the nice-to-use tvdb[1][24]['name] interface Since the nice-to-use tvdb[1][24]['name] interface
makes it impossible to do tvdb[1][24]['name] = "name" makes it impossible to do tvdb[1][24]['name] = "name"
@ -654,59 +627,57 @@ class Tvdb:
self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas]) self.shows[sid][seas][ep] = Episode(season=self.shows[sid][seas])
self.shows[sid][seas][ep][attrib] = value self.shows[sid][seas][ep][attrib] = value
def _setShowData(self, sid, key, value): def _set_show_data(self, sid, key, value):
"""Sets self.shows[sid] to a new Show instance, or sets the data """Sets self.shows[sid] to a new Show instance, or sets the data
""" """
if sid not in self.shows: if sid not in self.shows:
self.shows[sid] = Show() self.shows[sid] = Show()
self.shows[sid].data[key] = value self.shows[sid].data[key] = value
def _cleanData(self, data): def _clean_data(self, data):
"""Cleans up strings returned by TheTVDB.com """Cleans up strings returned by TheTVDB.com
Issues corrected: Issues corrected:
- Replaces &amp; with & - Replaces &amp; with &
- Trailing whitespace - Trailing whitespace
""" """
data = data.replace(u"&amp;", u"&") return data if data is None else data.strip().replace(u'&amp;', u'&')
data = data.strip()
return data
def search(self, series): def search(self, series):
"""This searches TheTVDB.com for the series name """This searches TheTVDB.com for the series name
and returns the result list and returns the result list
""" """
series = series.encode("utf-8") series = series.encode('utf-8')
log().debug("Searching for show %s" % series) log().debug('Searching for show %s' % series)
self.config['params_getSeries']['seriesname'] = series self.config['params_get_series']['seriesname'] = series
try: try:
seriesFound = self._getetsrc(self.config['url_getSeries'], self.config['params_getSeries']) series_found = self._getetsrc(self.config['url_get_series'], self.config['params_get_series'])
if seriesFound: if series_found:
return seriesFound.values()[0] return series_found.values()[0]
except: except:
pass pass
return [] return []
def _getSeries(self, series): def _get_series(self, series):
"""This searches TheTVDB.com for the series name, """This searches TheTVDB.com for the series name,
If a custom_ui UI is configured, it uses this to select the correct If a custom_ui UI is configured, it uses this to select the correct
series. If not, and interactive == True, ConsoleUI is used, if not series. If not, and interactive == True, ConsoleUI is used, if not
BaseUI is used to select the first result. BaseUI is used to select the first result.
""" """
allSeries = self.search(series) all_series = self.search(series)
if not isinstance(allSeries, list): if not isinstance(all_series, list):
allSeries = [allSeries] all_series = [all_series]
if len(allSeries) == 0: if 0 == len(all_series):
log().debug('Series result returned zero') log().debug('Series result returned zero')
raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)") raise tvdb_shownotfound('Show-name search returned zero results (cannot find show on TVDB)')
if self.config['custom_ui'] is not None: if None is not self.config['custom_ui']:
log().debug("Using custom UI %s" % (repr(self.config['custom_ui']))) log().debug('Using custom UI %s' % (repr(self.config['custom_ui'])))
CustomUI = self.config['custom_ui'] custom_ui = self.config['custom_ui']
ui = CustomUI(config=self.config) ui = custom_ui(config=self.config)
else: else:
if not self.config['interactive']: if not self.config['interactive']:
log().debug('Auto-selecting first search result using BaseUI') log().debug('Auto-selecting first search result using BaseUI')
@ -715,156 +686,151 @@ class Tvdb:
log().debug('Interactively selecting show using ConsoleUI') log().debug('Interactively selecting show using ConsoleUI')
ui = ConsoleUI(config=self.config) ui = ConsoleUI(config=self.config)
return ui.selectSeries(allSeries) return ui.selectSeries(all_series)
def _parseBanners(self, sid): def _parse_banners(self, sid):
"""Parses banners XML, from """Parses banners XML, from
http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
Banners are retrieved using t['show name]['_banners'], for example: Banners are retrieved using t['show name]['_banners'], for example:
>>> t = Tvdb(banners = True) >> t = Tvdb(banners = True)
>>> t['scrubs']['_banners'].keys() >> t['scrubs']['_banners'].keys()
['fanart', 'poster', 'series', 'season'] ['fanart', 'poster', 'series', 'season']
>>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath'] >> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
u'http://thetvdb.com/banners/posters/76156-2.jpg' u'http://thetvdb.com/banners/posters/76156-2.jpg'
>>> >>
Any key starting with an underscore has been processed (not the raw Any key starting with an underscore has been processed (not the raw
data from the XML) data from the XML)
This interface will be improved in future versions. This interface will be improved in future versions.
""" """
log().debug('Getting season banners for %s' % (sid)) log().debug('Getting season banners for %s' % sid)
bannersEt = self._getetsrc(self.config['url_seriesBanner'] % (sid)) banners_et = self._getetsrc(self.config['url_seriesBanner'] % sid)
banners = {} banners = {}
try: try:
for cur_banner in bannersEt['banner']: for cur_banner in banners_et['banner']:
bid = cur_banner['id'] bid = cur_banner['id']
btype = cur_banner['bannertype'] btype = cur_banner['bannertype']
btype2 = cur_banner['bannertype2'] btype2 = cur_banner['bannertype2']
if btype is None or btype2 is None: if None is btype or None is btype2:
continue continue
if not btype in banners: if btype not in banners:
banners[btype] = {} banners[btype] = {}
if not btype2 in banners[btype]: if btype2 not in banners[btype]:
banners[btype][btype2] = {} banners[btype][btype2] = {}
if not bid in banners[btype][btype2]: if bid not in banners[btype][btype2]:
banners[btype][btype2][bid] = {} banners[btype][btype2][bid] = {}
for k, v in cur_banner.items(): for k, v in cur_banner.items():
if k is None or v is None: if None is k or None is v:
continue continue
k, v = k.lower(), v.lower() k, v = k.lower(), v.lower()
banners[btype][btype2][bid][k] = v banners[btype][btype2][bid][k] = v
for k, v in banners[btype][btype2][bid].items(): for k, v in banners[btype][btype2][bid].items():
if k.endswith("path"): if k.endswith('path'):
new_key = "_%s" % (k) new_key = '_%s' % k
log().debug("Transforming %s to %s" % (k, new_key)) log().debug('Transforming %s to %s' % (k, new_key))
new_url = self.config['url_artworkPrefix'] % (v) new_url = self.config['url_artworkPrefix'] % v
banners[btype][btype2][bid][new_key] = new_url banners[btype][btype2][bid][new_key] = new_url
except: except:
pass pass
self._setShowData(sid, "_banners", banners) self._set_show_data(sid, '_banners', banners)
def _parseActors(self, sid): def _parse_actors(self, sid):
"""Parsers actors XML, from """Parsers actors XML, from
http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
Actors are retrieved using t['show name]['_actors'], for example: Actors are retrieved using t['show name]['_actors'], for example:
>>> t = Tvdb(actors = True) >> t = Tvdb(actors = True)
>>> actors = t['scrubs']['_actors'] >> actors = t['scrubs']['_actors']
>>> type(actors) >> type(actors)
<class 'tvdb_api.Actors'> <class 'tvdb_api.Actors'>
>>> type(actors[0]) >> type(actors[0])
<class 'tvdb_api.Actor'> <class 'tvdb_api.Actor'>
>>> actors[0] >> actors[0]
<Actor "Zach Braff"> <Actor "Zach Braff">
>>> sorted(actors[0].keys()) >> sorted(actors[0].keys())
['id', 'image', 'name', 'role', 'sortorder'] ['id', 'image', 'name', 'role', 'sortorder']
>>> actors[0]['name'] >> actors[0]['name']
u'Zach Braff' u'Zach Braff'
>>> actors[0]['image'] >> actors[0]['image']
u'http://thetvdb.com/banners/actors/43640.jpg' u'http://thetvdb.com/banners/actors/43640.jpg'
Any key starting with an underscore has been processed (not the raw Any key starting with an underscore has been processed (not the raw
data from the XML) data from the XML)
""" """
log().debug("Getting actors for %s" % (sid)) log().debug('Getting actors for %s' % sid)
actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid)) actors_et = self._getetsrc(self.config['url_actorsInfo'] % sid)
cur_actors = Actors() cur_actors = Actors()
try: try:
for curActorItem in actorsEt["actor"]: for curActorItem in actors_et['actor']:
curActor = Actor() cur_actor = Actor()
for k, v in curActorItem.items(): for k, v in curActorItem.items():
k = k.lower() k = k.lower()
if v is not None: if None is not v:
if k == "image": if 'image' == k:
v = self.config['url_artworkPrefix'] % (v) v = self.config['url_artworkPrefix'] % v
else: else:
v = self._cleanData(v) v = self._clean_data(v)
curActor[k] = v cur_actor[k] = v
cur_actors.append(curActor) cur_actors.append(cur_actor)
except: except:
pass pass
self._setShowData(sid, '_actors', cur_actors) self._set_show_data(sid, '_actors', cur_actors)
def _getShowData(self, sid, language, getEpInfo=False): def _get_show_data(self, sid, language, get_ep_info=False):
"""Takes a series ID, gets the epInfo URL and parses the TVDB """Takes a series ID, gets the epInfo URL and parses the TVDB
XML file into the shows dict in layout: XML file into the shows dict in layout:
shows[series_id][season_number][episode_number] shows[series_id][season_number][episode_number]
""" """
if self.config['language'] is None: if None is self.config['language']:
log().debug('Config language is none, using show language') log().debug('Config language is none, using show language')
if language is None: if None is language:
raise tvdb_error("config['language'] was None, this should not happen") raise tvdb_error('config[\'language\'] was None, this should not happen')
getShowInLanguage = language get_show_in_language = language
else: else:
log().debug( log().debug('Configured language %s override show language of %s' % (self.config['language'], language))
'Configured language %s override show language of %s' % ( get_show_in_language = self.config['language']
self.config['language'],
language
)
)
getShowInLanguage = self.config['language']
# Parse show information # Parse show information
log().debug('Getting all series data for %s' % (sid)) log().debug('Getting all series data for %s' % sid)
url = self.config['url_epInfo%s' % ('', '_zip')[self.config['useZip']]] % (sid, language) url = self.config['url_epInfo%s' % ('', '_zip')[self.config['useZip']]] % (sid, language)
show_data = self._getetsrc(url, language=getShowInLanguage) show_data = self._getetsrc(url, language=get_show_in_language)
# check and make sure we have data to process and that it contains a series name # check and make sure we have data to process and that it contains a series name
if not len(show_data) or (isinstance(show_data, dict) and 'seriesname' not in show_data['series']): if not len(show_data) or (isinstance(show_data, dict) and 'seriesname' not in show_data['series']):
return False return False
for k, v in show_data['series'].items(): for k, v in show_data['series'].items():
if v is not None: if None is not v:
if k in ['banner', 'fanart', 'poster']: if k in ['banner', 'fanart', 'poster']:
v = self.config['url_artworkPrefix'] % (v) v = self.config['url_artworkPrefix'] % v
else: else:
v = self._cleanData(v) v = self._clean_data(v)
self._setShowData(sid, k, v) self._set_show_data(sid, k, v)
if getEpInfo: if get_ep_info:
# Parse banners # Parse banners
if self.config['banners_enabled']: if self.config['banners_enabled']:
self._parseBanners(sid) self._parse_banners(sid)
# Parse actors # Parse actors
if self.config['actors_enabled']: if self.config['actors_enabled']:
self._parseActors(sid) self._parse_actors(sid)
# Parse episode data # Parse episode data
log().debug('Getting all episodes of %s' % (sid)) log().debug('Getting all episodes of %s' % sid)
if 'episode' not in show_data: if 'episode' not in show_data:
return False return False
@ -876,38 +842,38 @@ class Tvdb:
for cur_ep in episodes: for cur_ep in episodes:
if self.config['dvdorder']: if self.config['dvdorder']:
log().debug('Using DVD ordering.') log().debug('Using DVD ordering.')
use_dvd = cur_ep['dvd_season'] != None and cur_ep['dvd_episodenumber'] != None use_dvd = None is not cur_ep['dvd_season'] and None is not cur_ep['dvd_episodenumber']
else: else:
use_dvd = False use_dvd = False
if use_dvd: if use_dvd:
seasnum, epno = cur_ep['dvd_season'], cur_ep['dvd_episodenumber'] elem_seasnum, elem_epno = cur_ep['dvd_season'], cur_ep['dvd_episodenumber']
else: else:
seasnum, epno = cur_ep['seasonnumber'], cur_ep['episodenumber'] elem_seasnum, elem_epno = cur_ep['seasonnumber'], cur_ep['episodenumber']
if seasnum is None or epno is None: if None is elem_seasnum or None is elem_epno:
log().warning("An episode has incomplete season/episode number (season: %r, episode: %r)" % ( log().warning('An episode has incomplete season/episode number (season: %r, episode: %r)' % (
seasnum, epno)) elem_seasnum, elem_epno))
continue # Skip to next episode continue # Skip to next episode
# float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data # float() is because https://github.com/dbr/tvnamer/issues/95 - should probably be fixed in TVDB data
seas_no = int(float(seasnum)) seas_no = int(float(elem_seasnum))
ep_no = int(float(epno)) ep_no = int(float(elem_epno))
for k, v in cur_ep.items(): for k, v in cur_ep.items():
k = k.lower() k = k.lower()
if v is not None: if None is not v:
if k == 'filename': if 'filename' == k:
v = self.config['url_artworkPrefix'] % (v) v = self.config['url_artworkPrefix'] % v
else: else:
v = self._cleanData(v) v = self._clean_data(v)
self._setItem(sid, seas_no, ep_no, k, v) self._set_item(sid, seas_no, ep_no, k, v)
return True return True
def _nameToSid(self, name): def _name_to_sid(self, name):
"""Takes show name, returns the correct series ID (if the show has """Takes show name, returns the correct series ID (if the show has
already been grabbed), or grabs all episodes and returns already been grabbed), or grabs all episodes and returns
the correct SID. the correct SID.
@ -916,12 +882,12 @@ class Tvdb:
log().debug('Correcting %s to %s' % (name, self.corrections[name])) log().debug('Correcting %s to %s' % (name, self.corrections[name]))
return self.corrections[name] return self.corrections[name]
else: else:
log().debug('Getting show %s' % (name)) log().debug('Getting show %s' % name)
selected_series = self._getSeries(name) selected_series = self._get_series(name)
if isinstance(selected_series, dict): if isinstance(selected_series, dict):
selected_series = [selected_series] selected_series = [selected_series]
sids = list(int(x['id']) for x in selected_series if sids = list(int(x['id']) for x in selected_series if
self._getShowData(int(x['id']), self.config['language'])) self._get_show_data(int(x['id']), self.config['language']))
self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series)) self.corrections.update(dict((x['seriesname'], int(x['id'])) for x in selected_series))
return sids return sids
@ -932,19 +898,16 @@ class Tvdb:
if isinstance(key, (int, long)): if isinstance(key, (int, long)):
# Item is integer, treat as show id # Item is integer, treat as show id
if key not in self.shows: if key not in self.shows:
self._getShowData(key, self.config['language'], True) self._get_show_data(key, self.config['language'], True)
return None if key not in self.shows else self.shows[key] return None if key not in self.shows else self.shows[key]
key = str(key).lower() key = str(key).lower()
self.config['searchterm'] = key self.config['searchterm'] = key
selected_series = self._getSeries(key) selected_series = self._get_series(key)
if isinstance(selected_series, dict): if isinstance(selected_series, dict):
selected_series = [selected_series] selected_series = [selected_series]
[[self._setShowData(show['id'], k, v) for k, v in show.items()] for show in selected_series] [[self._set_show_data(show['id'], k, v) for k, v in show.items()] for show in selected_series]
return selected_series return selected_series
#test = self._getSeries(key)
#sids = self._nameToSid(key)
#return list(self.shows[sid] for sid in sids)
def __repr__(self): def __repr__(self):
return str(self.shows) return str(self.shows)
@ -963,5 +926,5 @@ def main():
print tvdb_instance['Lost'][1][4]['episodename'] print tvdb_instance['Lost'][1][4]['episodename']
if __name__ == '__main__': if '__main__' == __name__:
main() main()