diff --git a/couchpotato/core/media/show/_base/main.py b/couchpotato/core/media/show/_base/main.py
index c04bbc0e..5ed04127 100644
--- a/couchpotato/core/media/show/_base/main.py
+++ b/couchpotato/core/media/show/_base/main.py
@@ -1,16 +1,3 @@
-#from couchpotato.core.logger import CPLog
-#from couchpotato.core.media import MediaBase
-
-#log = CPLog(__name__)
-
-
-#class ShowBase(MediaBase):
-
- #identifier = 'show'
-
- #def __init__(self):
- #super(ShowBase, self).__init__()
-
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
@@ -96,32 +83,65 @@ class ShowBase(MediaBase):
'movie': movie_dict,
}
+ def debug(self):
+ """
+ XXX: This is only a hook for a breakpoint so we can test database stuff easily
+ REMOVE when finished
+ """
+ from couchpotato import get_session
+ from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
+ from couchpotato.core.helpers.encoding import toUnicode, simplifyString
+ from couchpotato.core.logger import CPLog
+ from couchpotato.core.plugins.base import Plugin
+ from couchpotato.core.settings.model import Library, LibraryTitle, File
+ from string import ascii_letters
+ import time
+ import traceback
+
+ db = get_session()
+ #parent = db.query(Library).filter_by(identifier = attrs.get('')).first()
+ return
+
def add(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
"""
1. Add Show
- 1. Add All Episodes
- 2. Add All Seasons
+ 2. Add All Episodes
+ 3. Add All Seasons
Notes, not to forget:
- relate parent and children, possible grandparent to grandchild so episodes know it belong to show, etc
- looks like we dont send info to library; it comes later
- change references to plot to description
- change Model to Media
+
+ params
+ {'category_id': u'-1',
+ 'identifier': u'tt1519931',
+ 'profile_id': u'12',
+ 'thetvdb_id': u'158661',
+ 'title': u'Haven'}
"""
log.debug("show.add")
-
- identifier = params.get('thetvdb_id')
+ # Add show parent to db first
+ parent = self.addToDatabase(params = params)
+
+ skip = False # XXX: For debugging
+ identifier = params.get('id')
episodes = fireEvent('show.episodes', identifier = identifier)
# XXX: Fix so we dont have a nested list
- for episode in episodes[0]:
- self.add2(params=episode)
+ if episodes is not None and skip is False:
+ for episode in episodes[0]:
+ episode['title'] = episode.get('titles', None)[0]
+ episode['identifier'] = episode.get('id', None)
+ episode['parent_identifier'] = identifier
+ self.addToDatabase(params=episode, type="episode")
+
+ return parent
- return self.add2(params = params)
-
- def add2(self, params = {}, force_readd = True, search_after = True, update_library = False, status_id = None):
- log.debug("show.add2")
+ def addToDatabase(self, params = {}, type="show", force_readd = True, search_after = True, update_library = False, status_id = None):
+ log.debug("show.addToDatabase")
if not params.get('identifier'):
msg = 'Can\'t add show without imdb identifier.'
@@ -139,7 +159,6 @@ class ShowBase(MediaBase):
#except:
#pass
-
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status
@@ -155,6 +174,7 @@ class ShowBase(MediaBase):
do_search = False
if not m:
m = Movie(
+ type = type,
library_id = library.get('id'),
profile_id = params.get('profile_id', default_profile.get('id')),
status_id = status_id if status_id else status_active.get('id'),
@@ -182,7 +202,7 @@ class ShowBase(MediaBase):
m.profile_id = params.get('profile_id', default_profile.get('id'))
m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None
else:
- log.debug('Movie already exists, not updating: %s', params)
+ log.debug('Show already exists, not updating: %s', params)
added = False
if force_readd:
@@ -211,22 +231,22 @@ class ShowBase(MediaBase):
db.expire_all()
return show_dict
- def createOnComplete(self, movie_id):
+ def createOnComplete(self, show_id):
def onComplete():
db = get_session()
- movie = db.query(Movie).filter_by(id = movie_id).first()
- fireEventAsync('movie.searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
+ show = db.query(Movie).filter_by(id = show_id).first()
+ fireEventAsync('show.searcher.single', show.to_dict(self.default_dict), on_complete = self.createNotifyFront(show_id))
db.expire_all()
return onComplete
- def createNotifyFront(self, movie_id):
+ def createNotifyFront(self, show_id):
def notifyFront():
db = get_session()
- movie = db.query(Movie).filter_by(id = movie_id).first()
- fireEvent('notify.frontend', type = 'show.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
+ show = db.query(Movie).filter_by(id = show_id).first()
+ fireEvent('notify.frontend', type = 'show.update.%s' % show.id, data = show.to_dict(self.default_dict))
db.expire_all()
return notifyFront
diff --git a/couchpotato/core/media/show/_base/static/search.js b/couchpotato/core/media/show/_base/static/search.js
index 89530858..6a089f89 100644
--- a/couchpotato/core/media/show/_base/static/search.js
+++ b/couchpotato/core/media/show/_base/static/search.js
@@ -202,7 +202,7 @@ Block.ShowSearch.Item = new Class({
info = self.info;
self.el = new Element('div.show_result', {
- 'id': info.imdb
+ 'id': info.id
}).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
@@ -283,8 +283,10 @@ Block.ShowSearch.Item = new Class({
Api.request('show.add', {
'data': {
- 'identifier': self.info.imdb,
- 'thetvdb_id': self.info.thetvdb_id,
+ 'identifier': self.info.id,
+ 'id': self.info.id,
+ 'type': self.info.type,
+ 'primary_provider': self.info.primary_provider,
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value'),
'category_id': self.category_select.get('value')
diff --git a/couchpotato/core/plugins/library/main.py b/couchpotato/core/plugins/library/main.py
index 892497d8..fbaafbdb 100644
--- a/couchpotato/core/plugins/library/main.py
+++ b/couchpotato/core/plugins/library/main.py
@@ -20,22 +20,36 @@ class LibraryPlugin(Plugin):
addEvent('library.update', self.update)
addEvent('library.update_release_date', self.updateReleaseDate)
- def add(self, attrs = {}, update_after = True, type='movie'):
+ def add(self, attrs = {}, update_after = True):
+ # movies don't yet contain these, so lets make sure to set defaults
+ type = attrs.get('type', 'movie')
+ primary_provider = attrs.get('primary_provider', 'imdb')
+
db = get_session()
+ parent_identifier = attrs.get('parent_identifier', None)
+
+ # XXX: add type (somehow? it will be show when episode id the type) so we dont get conflicts
+ parent = None
+ if parent_identifier:
+ #parent = db.query(Library).filter_by(identifier = attrs.get('parent_identifier')).first()
+ parent = db.query(Library).filter_by(primary_provider = primary_provider, identifier = attrs.get('parent_identifier')).first()
- l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
+ l = db.query(Library).filter_by(type = type, identifier = attrs.get('identifier')).first()
if not l:
status = fireEvent('status.get', 'needs_update', single = True)
l = Library(
type = type,
+ primary_provider = primary_provider,
year = attrs.get('year'),
identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'),
info = {},
+ parent = parent,
)
+ # children = [],
title = LibraryTitle(
title = toUnicode(attrs.get('title')),
@@ -71,10 +85,20 @@ class LibraryPlugin(Plugin):
do_update = True
+ # XXX: Fix to be pretty
+ parent_identifier = None
+ if library.parent:
+ parent_identifier = library.parent.identifier
+
if library.status_id == done_status.get('id') and not force:
do_update = False
+ # XXX: do this a better way. we need to pass parent_identifier to things like episode.info
+ # maybe just make all .info (movie, show, season and esisode requre parent var and not use it)
+ elif parent_identifier:
+ info = fireEvent('%s.info' % library.type, merge = True, identifier = identifier, \
+ parent_identifier = parent_identifier)
else:
- info = fireEvent('movie.info', merge = True, identifier = identifier)
+ info = fireEvent('%s.info' % library.type, merge = True, identifier = identifier)
# Don't need those here
try: del info['in_wanted']
diff --git a/couchpotato/core/providers/show/thetvdb/main.py b/couchpotato/core/providers/show/thetvdb/main.py
index 321fe36c..fa19580c 100644
--- a/couchpotato/core/providers/show/thetvdb/main.py
+++ b/couchpotato/core/providers/show/thetvdb/main.py
@@ -2,24 +2,33 @@ from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.show.base import ShowProvider
-from thetvdb.tvdb_api import Tvdb
+from tvdb_api import tvdb_api, tvdb_exceptions
+from datetime import datetime
import traceback
log = CPLog(__name__)
-
+# XXX: I return None in alot of functions when there is error or no value; check if I
+# should be returning an empty list or dictionary
+# XXX: Consider grabbing zips to put less strain on tvdb
+# XXX: Consider a cache; not implenented everywhere yet or at all
class TheTVDb(ShowProvider):
def __init__(self):
#addEvent('show.by_hash', self.byHash)
addEvent('show.search', self.search, priority = 1)
- addEvent('show.info', self.getInfo, priority = 1)
+ addEvent('show.info', self.getShowInfo, priority = 1)
addEvent('show.episodes', self.getEpisodes, priority = 1)
+ addEvent('episode.info', self.getEpisodeInfo, priority = 1)
#addEvent('show.info_by_thetvdb', self.getInfoByTheTVDBId)
- # Use base wrapper
- #thetvdbtmdb.configure(self.conf('api_key'))
- self.tvdb = Tvdb(apikey="7966C02F860586D2", banners=True)
+ # XXX: Load from somewhere else
+ tvdb_api_parms = {
+ 'apikey':"7966C02F860586D2",
+ 'banners':True
+ }
+
+ self.tvdb = tvdb_api.Tvdb(**tvdb_api_parms)
#def byHash(self, file):
#''' Find show by hash '''
@@ -75,8 +84,9 @@ class TheTVDb(ShowProvider):
try:
raw = self.tvdb.search(search_string)
- except: # XXX: Make more specific
+ except (tvdb_exceptions.tvdb_error, IOError), e:
log.error('Failed searching TheTVDB for "%s": %s', (search_string, traceback.format_exc()))
+ return None
results = []
if raw:
@@ -95,51 +105,87 @@ class TheTVDb(ShowProvider):
self.setCache(cache_key, results)
return results
- except SyntaxError, e:
- log.error('Failed to parse XML response: %s', e)
+ #except SyntaxError, e:
+ # log.error('Failed to parse XML response: %s', e)
+ # return False
+ except (tvdb_exceptions.tvdb_error, IOError), e:
+ log.error('Failed parsing TheTVDB for "%s": %s', (show, traceback.format_exc()))
return False
return results
- def getEpisodes(self, identifier=None):
+ def getEpisodes(self, identifier=None, episode_identifier=None):
+ """Either return a list of all episodes or a single episode.
+ If episode_identifer contains an episode to search for it will be returned if found
+ """
if not identifier:
- return []
+ return None
try:
show = self.tvdb[int(identifier)]
- except:
- return []
+ except (tvdb_exceptions.tvdb_error, IOError), e:
+ log.error('Failed parsing TheTVDB for "%s" id "%s": %s', (show, identifier, traceback.format_exc()))
+ return None
result = []
for season in show.values():
for episode in season.values():
- # Consider cache
- result.append(self.parseEpisode(episode))
+ if episode_identifier:
+ if episode['id'] == toUnicode(episode_identifier):
+ return episode
+ else:
+ result.append(self.parseEpisode(show, episode))
return result
- def getInfo(self, identifier = None):
+ def getShow(self, identifier = None):
+ show = None
+ try:
+ log.debug('Getting show: %s', identifier)
+ show = self.tvdb[int(identifier)]
+ except (tvdb_exceptions.tvdb_error, IOError), e:
+ log.error('Failed to getShowInfo for show id "%s": %s', (identifier, traceback.format_exc()))
+ return None
+
+ return show
+
+ def getShowInfo(self, identifier = None):
+ if not identifier:
+ return None
+
cache_key = 'thetvdb.cache.%s' % identifier
- result = self.getCache(cache_key)
-
- if not result:
- result = {}
- show = None
-
- try:
- log.debug('Getting info: %s', cache_key)
- #show = thetvdb.imdbLookup(id = identifier)
- show = self.tvdb[int(identifier)]
- except:
- pass
-
- if show:
- #result = self.parseShow(show[0])
- result = self.parseShow(show)
- self.setCache(cache_key, result)
+ log.debug('Getting showInfo: %s', cache_key)
+ result = self.getCache(cache_key) or {}
+ if result:
+ return result
+
+ show = self.getShow(identifier=identifier)
+ if show:
+ result = self.parseShow(show)
+ self.setCache(cache_key, result)
return result
+ def getEpisodeInfo(self, identifier = None, parent_identifier = None):
+ if not identifier or not parent_identifier:
+ return None
+
+ cache_key = 'thetvdb.cache.%s.%s' % (parent_identifier, identifier)
+ log.debug('Getting EpisodeInfo: %s', cache_key)
+ result = self.getCache(cache_key) or {}
+ if result:
+ return result
+
+ show = self.getShow(identifier = parent_identifier)
+ if show:
+ episode = self.getEpisodes(identifier=parent_identifier, episode_identifier=identifier)
+
+ if episode:
+ result = self.parseEpisode(show, episode)
+ self.setCache(cache_key, result)
+
+ return result
+
#def getInfoByTheTVDBId(self, id = None):
#cache_key = 'thetvdb.cache.%s' % id
@@ -191,6 +237,10 @@ class TheTVDb(ShowProvider):
'zap2it_id': u'SH01009396'}
"""
+ # Make sure we have a valid show id, not '' or None
+ #if len (show['id']) is 0:
+ # return None
+
## Images
poster = self.getImage(show, type = 'poster', size = 'cover')
backdrop = self.getImage(show, type = 'fanart', size = 'w1280')
@@ -200,12 +250,16 @@ class TheTVDb(ShowProvider):
## Genres
genres = [] if show['genre'] is None else show['genre'].strip('|').split('|')
- ## Year (not really needed for show)
- year = None
+ ## Year
+ if show['firstaired']:
+ year = datetime.strptime(show['firstaired'], '%Y-%m-%d').year
+ else:
+ year = None
show_data = {
- 'via_thetvdb': True,
- 'thetvdb_id': int(show['id']),
+ 'id': int(show['id']),
+ 'type': 'show',
+ 'primary_provider': 'thetvdb',
'titles': [show['seriesname'], ],
'original_title': show['seriesname'],
'images': {
@@ -232,7 +286,7 @@ class TheTVDb(ShowProvider):
return show_data
- def parseEpisode(self, episode):
+ def parseEpisode(self, show, episode):
"""
('episodenumber', u'1'),
('thumb_added', None),
@@ -273,16 +327,24 @@ class TheTVDb(ShowProvider):
#backdrop = self.getImage(episode, type = 'fanart', size = 'w1280')
##poster_original = self.getImage(episode, type = 'poster', size = 'original')
##backdrop_original = self.getImage(episode, type = 'backdrop', size = 'original')
- poster = []
+ poster = episode['filename'] or []
backdrop = []
## Genres
genres = []
+
+ plot = "%s - %sx%s - %s" % (show['seriesname'], episode['seasonnumber'], episode['episodenumber'], episode['overview'])
- ## Year (not really needed for episode)
- year = None
+ ## Year
+ if episode['firstaired']:
+ year = datetime.strptime(episode['firstaired'], '%Y-%m-%d').year
+ else:
+ year = None
episode_data = {
+ 'id': int(episode['id']),
+ 'type': 'episode',
+ 'primary_provider': 'thetvdb',
'via_thetvdb': True,
'thetvdb_id': int(episode['id']),
'titles': [episode['episodename'], ],
@@ -297,8 +359,9 @@ class TheTVDb(ShowProvider):
'runtime': None,
'released': episode['firstaired'],
'year': year,
- 'plot': episode['overview'],
+ 'plot': plot,
'genres': genres,
+ 'parent_identifier': show['id'],
}
episode_data = dict((k, v) for k, v in episode_data.iteritems() if v)
diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index 57ce180a..5013d3ad 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -92,7 +92,7 @@ class Library(Entity):
# For Movies, CPS uses three: omdbapi (no prio !?), tmdb (prio 2) and couchpotatoapi (prio 1)
type = Field(String(10), default="movie", index=True)
- provider = Field(String(10), default="imdb", index=True)
+ primary_provider = Field(String(10), default="imdb", index=True)
year = Field(Integer)
identifier = Field(String(20), index = True)
@@ -104,6 +104,9 @@ class Library(Entity):
movies = OneToMany('Movie', cascade = 'all, delete-orphan')
titles = OneToMany('LibraryTitle', cascade = 'all, delete-orphan')
files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
+
+ parent = ManyToOne('Library')
+ children = OneToMany('Library')
class LibraryTitle(Entity):
@@ -118,142 +121,6 @@ class LibraryTitle(Entity):
libraries = ManyToOne('Library')
-#class Show(Entity):
- #"""Combined Show and Library"""
-
- #using_options(order_by = '-default') # ???
-
- #last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
- ##identifier = Field(String(20), index = True)
-
- #title = Field(Unicode) # Show title
- #simple_title = Field(Unicode, index = True) # Simple show title
- #default = Field(Boolean, default = False, index = True) # ???
-
- ### Wont need the following commented out vars since a show can not be downloaded,
- ### only episodes can be
- ###status = ManyToOne('Status') # Download, watched, etc
- ###releases = OneToMany('Release', cascade = 'all, delete-orphan') # List all available releases that can be downloaded?
- ###files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True) # File on hard drive
- #profile = ManyToOne('Profile') # ??? Quality ???
- #category = ManyToOne('Category') # ???
- #language = OneToMany('Language') # Language ??? (en) ???
-
- ## New fields
- #air_by_date = Field(Boolean, default=False) # True if no season or episode number
- #original_air_date = Field(Integer) # First date ever released
- #year = Field(Integer) # 1983
- #air_day = Field(Integer) # Monday, Tuesday...
- #air_time = Field(Integer) # 8PM EST
- #series_id = Field(Integer) # Series id
- #show_stauts = Field(Integer) # Continuing, Ended
-
- #duration = Field(Integer) # Length of show in seconds
- #summary = Field(Unicode) # Description of show
- #network = Field(Unicode) # ABC, Fox
- #rating = Field(Float) # 0.000-10.000 (star rating)
- #content_rating = Field(Unicode) # "TV-PG"
-
- #default_provider = Field(Integer, default=0)# thetvdb for example; allows per show providers
-
- #genre = ManyToMany('Genre') # Genre (comedy, etc)
- #episodes = OneToMany('Episode') # All the episodes that belong to this show
- #seasons = ManyToOne('Season') # Seasons artwork
- #banners = ManyToOne('Banner') # Banner artwork
- #posters = ManyToOne('Poster') # Poster artwork
- #fanart = ManyToOne('Fanart') # Fanart artwork
- #actors = ManyToMany('Actor') # Actor info and artwork
- #provider_ids = ManyToMany('ProviderIds') # 'imdb_id', 'zap2it_id', 'tvrage'
- #titles = OneToMany('ShowTitle', cascade = 'all, delete-orphan')
-
-
-#class ShowTitle(Entity):
- #""""""
- #using_options(order_by = '-default')
-
- #title = Field(Unicode)
- #simple_title = Field(Unicode, index = True)
- #default = Field(Boolean, default = False, index = True)
-
- #language = OneToMany('Language')
- #shows = ManyToOne('Show')
-
-
-#class Episode(Entity):
- #"""Combined Show and Library"""
-
- ##using_options(order_by = '-default') # ???
- ##identifier = Field(String(20), index = True)
-
- #last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
- #title = Field(Unicode) # Show title
- #simple_title = Field(Unicode, index = True) # Simple show title
- #default = Field(Boolean, default = False, index = True) # ???
-
- #status = ManyToOne('Status') # Download, watched, etc
- #profile = ManyToOne('Profile') # ??? Quality ???
- #category = ManyToOne('Category') # ???
- #releases = OneToMany('Release', cascade = 'all, delete-orphan') # List all available releases that can be downloaded?
- #files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True) # File on hard drive
- #language = OneToMany('Language') # Language ??? (en) ???
-
- ## New fields
- #season = Field(Integer) # Season number
- #number = Field(Integer) # Episode number
- #image = Field(BLOB) # Episode Image (XXX: What to do with images?)
- #air_date = Field(Integer) # Origianl air date
- #duration = Field(Integer) # Length of show (24:34) in seconds
- #summary = Field(Unicode) # Description of show
- #rating = Field(Float) # 0.000-10.000 (star rating)
- #content_rating = Field(Unicode) # "TV-PG"
- #production_code = Field(Unicode) # Production code (should this be an Integer)
-
- #show = ManyToOne('Show') # Parent show
- #actors = ManyToMany('Actor') # Guest Actor info and artwork
- #directors = ManyToMany('Director') # Directors of episode
- #writers = ManyToMany('Writer') # Writers of episode
- #provider_ids = ManyToMany('ProviderIds') # 'imdb_id', 'zap2it_id', 'tvrage'
-
-
-#class Fanart(Entity):
- #"""Stub for Now"""
- #show = OneToMany('Show')
-
-#class Actor(Entity):
- #"""Stub for Now"""
- #shows = ManyToMany('Show')
- #episodes = ManyToMany('Episode')
-
-#class Director(Entity):
- #"""Stub for Now"""
- #episodes = ManyToMany('Episode')
-
-#class Writer(Entity):
- #"""Stub for Now"""
- #episodes = ManyToMany('Episode')
-
-#class Genre(Entity):
- #"""Stub for Now"""
- #shows = ManyToMany('Show')
-
-#class Season(Entity):
- #"""Stub for Now"""
- #show = OneToMany('Show')
-
-#class Banner(Entity):
- #"""Stub for Now"""
- #show = OneToMany('Show')
-
-#class Poster(Entity):
- #"""Stub for Now"""
- #show = OneToMany('Show')
-
-#class ProviderIds(Entity):
- #"""Stub for Now"""
- #shows = ManyToMany('Show')
- #episodes = ManyToMany('Episode')
-
-
class Language(Entity):
""""""
@@ -261,9 +128,6 @@ class Language(Entity):
label = Field(Unicode)
titles = ManyToOne('LibraryTitle')
- #show_titles = ManyToOne('ShowTitle')
- #show = ManyToOne('Show')
- #episode = ManyToOne('Episode')
class Release(Entity):
@@ -274,7 +138,6 @@ class Release(Entity):
identifier = Field(String(100), index = True)
movie = ManyToOne('Movie')
- #episode = ManyToOne('Episode')
status = ManyToOne('Status')
quality = ManyToOne('Quality')
files = ManyToMany('File')
@@ -313,8 +176,6 @@ class Status(Entity):
label = Field(Unicode(20))
releases = OneToMany('Release')
- #movies = OneToMany('Movie')
- #episodes = OneToMany('Episode')
class Quality(Entity):
@@ -342,8 +203,6 @@ class Profile(Entity):
hide = Field(Boolean, default = False)
movie = OneToMany('Movie')
- #show = OneToMany('Show')
- #episode = OneToMany('Episode')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
def to_dict(self, deep = {}, exclude = []):
@@ -365,8 +224,6 @@ class Category(Entity):
destination = Field(Unicode(255))
movie = OneToMany('Movie')
- #show = OneToMany('Show')
- #episode = OneToMany('Episode')
destination = Field(Unicode(255))
@@ -394,7 +251,6 @@ class File(Entity):
history = OneToMany('RenameHistory')
movie = ManyToMany('Movie')
- #episodes = ManyToMany('Episode')
release = ManyToMany('Release')
library = ManyToMany('Library')
diff --git a/libs/thetvdb/.gitignore b/libs/thetvdb/.gitignore
deleted file mode 100644
index e42c383b..00000000
--- a/libs/thetvdb/.gitignore
+++ /dev/null
@@ -1,4 +0,0 @@
-.DS_Store
-*.pyc
-*.egg-info/*
-dist/*.tar.gz
diff --git a/libs/thetvdb/.travis.yml b/libs/thetvdb/.travis.yml
deleted file mode 100644
index 36f5e510..00000000
--- a/libs/thetvdb/.travis.yml
+++ /dev/null
@@ -1,9 +0,0 @@
-language: python
-python:
- - 2.5
- - 2.6
- - 2.7
-
-install: pip install nose
-
-script: nosetests
diff --git a/libs/thetvdb/MANIFEST.in b/libs/thetvdb/MANIFEST.in
deleted file mode 100644
index bd227aa4..00000000
--- a/libs/thetvdb/MANIFEST.in
+++ /dev/null
@@ -1,4 +0,0 @@
-include UNLICENSE
-include readme.md
-include tests/*.py
-include Rakefile
diff --git a/libs/thetvdb/Rakefile b/libs/thetvdb/Rakefile
deleted file mode 100644
index 561deb70..00000000
--- a/libs/thetvdb/Rakefile
+++ /dev/null
@@ -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
diff --git a/libs/thetvdb/UNLICENSE b/libs/thetvdb/UNLICENSE
deleted file mode 100644
index c4205d41..00000000
--- a/libs/thetvdb/UNLICENSE
+++ /dev/null
@@ -1,26 +0,0 @@
-Copyright 2011-2012 Ben Dickson (dbr)
-
-This is free and unencumbered software released into the public domain.
-
-Anyone is free to copy, modify, publish, use, compile, sell, or
-distribute this software, either in source code form or as a compiled
-binary, for any purpose, commercial or non-commercial, and by any
-means.
-
-In jurisdictions that recognize copyright laws, the author or authors
-of this software dedicate any and all copyright interest in the
-software to the public domain. We make this dedication for the benefit
-of the public at large and to the detriment of our heirs and
-successors. We intend this dedication to be an overt act of
-relinquishment in perpetuity of all present and future rights to this
-software under copyright law.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
-IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
-OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
-ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
-OTHER DEALINGS IN THE SOFTWARE.
-
-For more information, please refer to
diff --git a/libs/thetvdb/__init__.py b/libs/thetvdb/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/libs/thetvdb/readme.md b/libs/thetvdb/readme.md
deleted file mode 100644
index a34726e5..00000000
--- a/libs/thetvdb/readme.md
+++ /dev/null
@@ -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`)
-
-[](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 = tvdb_api.Tvdb()
- 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 = tvdb_api.Tvdb()
- >>> show = t['scrubs']
- >>> type(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', '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]
-
- >>> 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
diff --git a/libs/thetvdb/setup.py b/libs/thetvdb/setup.py
deleted file mode 100644
index 18eb7d1a..00000000
--- a/libs/thetvdb/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-from setuptools import setup
-setup(
-name = 'tvdb_api',
-version='1.8.2',
-
-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
-
->>> 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",
-]
-)
diff --git a/libs/thetvdb/tests/gprof2dot.py b/libs/thetvdb/tests/gprof2dot.py
deleted file mode 100644
index 4503ec7a..00000000
--- a/libs/thetvdb/tests/gprof2dot.py
+++ /dev/null
@@ -1,1638 +0,0 @@
-#!/usr/bin/env python
-#
-# Copyright 2008 Jose Fonseca
-#
-# This program is free software: you can redistribute it and/or modify it
-# under the terms of the GNU Lesser General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Lesser General Public License for more details.
-#
-# You should have received a copy of the GNU Lesser General Public License
-# along with this program. If not, see .
-#
-
-"""Generate a dot graph from the output of several profilers."""
-
-__author__ = "Jose Fonseca"
-
-__version__ = "1.0"
-
-
-import sys
-import math
-import os.path
-import re
-import textwrap
-import optparse
-
-
-try:
- # Debugging helper module
- import debug
-except ImportError:
- pass
-
-
-def percentage(p):
- return "%.02f%%" % (p*100.0,)
-
-def add(a, b):
- return a + b
-
-def equal(a, b):
- if a == b:
- return a
- else:
- return None
-
-def fail(a, b):
- assert False
-
-
-def ratio(numerator, denominator):
- numerator = float(numerator)
- denominator = float(denominator)
- assert 0.0 <= numerator
- assert numerator <= denominator
- try:
- return numerator/denominator
- except ZeroDivisionError:
- # 0/0 is undefined, but 1.0 yields more useful results
- return 1.0
-
-
-class UndefinedEvent(Exception):
- """Raised when attempting to get an event which is undefined."""
-
- def __init__(self, event):
- Exception.__init__(self)
- self.event = event
-
- def __str__(self):
- return 'unspecified event %s' % self.event.name
-
-
-class Event(object):
- """Describe a kind of event, and its basic operations."""
-
- def __init__(self, name, null, aggregator, formatter = str):
- self.name = name
- self._null = null
- self._aggregator = aggregator
- self._formatter = formatter
-
- def __eq__(self, other):
- return self is other
-
- def __hash__(self):
- return id(self)
-
- def null(self):
- return self._null
-
- def aggregate(self, val1, val2):
- """Aggregate two event values."""
- assert val1 is not None
- assert val2 is not None
- return self._aggregator(val1, val2)
-
- def format(self, val):
- """Format an event value."""
- assert val is not None
- return self._formatter(val)
-
-
-MODULE = Event("Module", None, equal)
-PROCESS = Event("Process", None, equal)
-
-CALLS = Event("Calls", 0, add)
-SAMPLES = Event("Samples", 0, add)
-
-TIME = Event("Time", 0.0, add, lambda x: '(' + str(x) + ')')
-TIME_RATIO = Event("Time ratio", 0.0, add, lambda x: '(' + percentage(x) + ')')
-TOTAL_TIME = Event("Total time", 0.0, fail)
-TOTAL_TIME_RATIO = Event("Total time ratio", 0.0, fail, percentage)
-
-CALL_RATIO = Event("Call ratio", 0.0, add, percentage)
-
-PRUNE_RATIO = Event("Prune ratio", 0.0, add, percentage)
-
-
-class Object(object):
- """Base class for all objects in profile which can store events."""
-
- def __init__(self, events=None):
- if events is None:
- self.events = {}
- else:
- self.events = events
-
- def __hash__(self):
- return id(self)
-
- def __eq__(self, other):
- return self is other
-
- def __contains__(self, event):
- return event in self.events
-
- def __getitem__(self, event):
- try:
- return self.events[event]
- except KeyError:
- raise UndefinedEvent(event)
-
- def __setitem__(self, event, value):
- if value is None:
- if event in self.events:
- del self.events[event]
- else:
- self.events[event] = value
-
-
-class Call(Object):
- """A call between functions.
-
- There should be at most one call object for every pair of functions.
- """
-
- def __init__(self, callee_id):
- Object.__init__(self)
- self.callee_id = callee_id
-
-
-class Function(Object):
- """A function."""
-
- def __init__(self, id, name):
- Object.__init__(self)
- self.id = id
- self.name = name
- self.calls = {}
- self.cycle = None
-
- def add_call(self, call):
- if call.callee_id in self.calls:
- sys.stderr.write('warning: overwriting call from function %s to %s\n' % (str(self.id), str(call.callee_id)))
- self.calls[call.callee_id] = call
-
- # TODO: write utility functions
-
- def __repr__(self):
- return self.name
-
-
-class Cycle(Object):
- """A cycle made from recursive function calls."""
-
- def __init__(self):
- Object.__init__(self)
- # XXX: Do cycles need an id?
- self.functions = set()
-
- def add_function(self, function):
- assert function not in self.functions
- self.functions.add(function)
- # XXX: Aggregate events?
- if function.cycle is not None:
- for other in function.cycle.functions:
- if function not in self.functions:
- self.add_function(other)
- function.cycle = self
-
-
-class Profile(Object):
- """The whole profile."""
-
- def __init__(self):
- Object.__init__(self)
- self.functions = {}
- self.cycles = []
-
- def add_function(self, function):
- if function.id in self.functions:
- sys.stderr.write('warning: overwriting function %s (id %s)\n' % (function.name, str(function.id)))
- self.functions[function.id] = function
-
- def add_cycle(self, cycle):
- self.cycles.append(cycle)
-
- def validate(self):
- """Validate the edges."""
-
- for function in self.functions.itervalues():
- for callee_id in function.calls.keys():
- assert function.calls[callee_id].callee_id == callee_id
- if callee_id not in self.functions:
- sys.stderr.write('warning: call to undefined function %s from function %s\n' % (str(callee_id), function.name))
- del function.calls[callee_id]
-
- def find_cycles(self):
- """Find cycles using Tarjan's strongly connected components algorithm."""
-
- # Apply the Tarjan's algorithm successively until all functions are visited
- visited = set()
- for function in self.functions.itervalues():
- if function not in visited:
- self._tarjan(function, 0, [], {}, {}, visited)
- cycles = []
- for function in self.functions.itervalues():
- if function.cycle is not None and function.cycle not in cycles:
- cycles.append(function.cycle)
- self.cycles = cycles
- if 0:
- for cycle in cycles:
- sys.stderr.write("Cycle:\n")
- for member in cycle.functions:
- sys.stderr.write("\t%s\n" % member.name)
-
- def _tarjan(self, function, order, stack, orders, lowlinks, visited):
- """Tarjan's strongly connected components algorithm.
-
- See also:
- - http://en.wikipedia.org/wiki/Tarjan's_strongly_connected_components_algorithm
- """
-
- visited.add(function)
- orders[function] = order
- lowlinks[function] = order
- order += 1
- pos = len(stack)
- stack.append(function)
- for call in function.calls.itervalues():
- callee = self.functions[call.callee_id]
- # TODO: use a set to optimize lookup
- if callee not in orders:
- order = self._tarjan(callee, order, stack, orders, lowlinks, visited)
- lowlinks[function] = min(lowlinks[function], lowlinks[callee])
- elif callee in stack:
- lowlinks[function] = min(lowlinks[function], orders[callee])
- if lowlinks[function] == orders[function]:
- # Strongly connected component found
- members = stack[pos:]
- del stack[pos:]
- if len(members) > 1:
- cycle = Cycle()
- for member in members:
- cycle.add_function(member)
- return order
-
- def call_ratios(self, event):
- # Aggregate for incoming calls
- cycle_totals = {}
- for cycle in self.cycles:
- cycle_totals[cycle] = 0.0
- function_totals = {}
- for function in self.functions.itervalues():
- function_totals[function] = 0.0
- for function in self.functions.itervalues():
- for call in function.calls.itervalues():
- if call.callee_id != function.id:
- callee = self.functions[call.callee_id]
- function_totals[callee] += call[event]
- if callee.cycle is not None and callee.cycle is not function.cycle:
- cycle_totals[callee.cycle] += call[event]
-
- # Compute the ratios
- for function in self.functions.itervalues():
- for call in function.calls.itervalues():
- assert CALL_RATIO not in call
- if call.callee_id != function.id:
- callee = self.functions[call.callee_id]
- if callee.cycle is not None and callee.cycle is not function.cycle:
- total = cycle_totals[callee.cycle]
- else:
- total = function_totals[callee]
- call[CALL_RATIO] = ratio(call[event], total)
-
- def integrate(self, outevent, inevent):
- """Propagate function time ratio allong the function calls.
-
- Must be called after finding the cycles.
-
- See also:
- - http://citeseer.ist.psu.edu/graham82gprof.html
- """
-
- # Sanity checking
- assert outevent not in self
- for function in self.functions.itervalues():
- assert outevent not in function
- assert inevent in function
- for call in function.calls.itervalues():
- assert outevent not in call
- if call.callee_id != function.id:
- assert CALL_RATIO in call
-
- # Aggregate the input for each cycle
- for cycle in self.cycles:
- total = inevent.null()
- for function in self.functions.itervalues():
- total = inevent.aggregate(total, function[inevent])
- self[inevent] = total
-
- # Integrate along the edges
- total = inevent.null()
- for function in self.functions.itervalues():
- total = inevent.aggregate(total, function[inevent])
- self._integrate_function(function, outevent, inevent)
- self[outevent] = total
-
- def _integrate_function(self, function, outevent, inevent):
- if function.cycle is not None:
- return self._integrate_cycle(function.cycle, outevent, inevent)
- else:
- if outevent not in function:
- total = function[inevent]
- for call in function.calls.itervalues():
- if call.callee_id != function.id:
- total += self._integrate_call(call, outevent, inevent)
- function[outevent] = total
- return function[outevent]
-
- def _integrate_call(self, call, outevent, inevent):
- assert outevent not in call
- assert CALL_RATIO in call
- callee = self.functions[call.callee_id]
- subtotal = call[CALL_RATIO]*self._integrate_function(callee, outevent, inevent)
- call[outevent] = subtotal
- return subtotal
-
- def _integrate_cycle(self, cycle, outevent, inevent):
- if outevent not in cycle:
-
- total = inevent.null()
- for member in cycle.functions:
- subtotal = member[inevent]
- for call in member.calls.itervalues():
- callee = self.functions[call.callee_id]
- if callee.cycle is not cycle:
- subtotal += self._integrate_call(call, outevent, inevent)
- total += subtotal
- cycle[outevent] = total
-
- callees = {}
- for function in self.functions.itervalues():
- if function.cycle is not cycle:
- for call in function.calls.itervalues():
- callee = self.functions[call.callee_id]
- if callee.cycle is cycle:
- try:
- callees[callee] += call[CALL_RATIO]
- except KeyError:
- callees[callee] = call[CALL_RATIO]
-
- for callee, call_ratio in callees.iteritems():
- ranks = {}
- call_ratios = {}
- partials = {}
- self._rank_cycle_function(cycle, callee, 0, ranks)
- self._call_ratios_cycle(cycle, callee, ranks, call_ratios, set())
- partial = self._integrate_cycle_function(cycle, callee, call_ratio, partials, ranks, call_ratios, outevent, inevent)
- assert partial == max(partials.values())
- assert not total or abs(1.0 - partial/(call_ratio*total)) <= 0.001
-
- return cycle[outevent]
-
- def _rank_cycle_function(self, cycle, function, rank, ranks):
- if function not in ranks or ranks[function] > rank:
- ranks[function] = rank
- for call in function.calls.itervalues():
- if call.callee_id != function.id:
- callee = self.functions[call.callee_id]
- if callee.cycle is cycle:
- self._rank_cycle_function(cycle, callee, rank + 1, ranks)
-
- def _call_ratios_cycle(self, cycle, function, ranks, call_ratios, visited):
- if function not in visited:
- visited.add(function)
- for call in function.calls.itervalues():
- if call.callee_id != function.id:
- callee = self.functions[call.callee_id]
- if callee.cycle is cycle:
- if ranks[callee] > ranks[function]:
- call_ratios[callee] = call_ratios.get(callee, 0.0) + call[CALL_RATIO]
- self._call_ratios_cycle(cycle, callee, ranks, call_ratios, visited)
-
- def _integrate_cycle_function(self, cycle, function, partial_ratio, partials, ranks, call_ratios, outevent, inevent):
- if function not in partials:
- partial = partial_ratio*function[inevent]
- for call in function.calls.itervalues():
- if call.callee_id != function.id:
- callee = self.functions[call.callee_id]
- if callee.cycle is not cycle:
- assert outevent in call
- partial += partial_ratio*call[outevent]
- else:
- if ranks[callee] > ranks[function]:
- callee_partial = self._integrate_cycle_function(cycle, callee, partial_ratio, partials, ranks, call_ratios, outevent, inevent)
- call_ratio = ratio(call[CALL_RATIO], call_ratios[callee])
- call_partial = call_ratio*callee_partial
- try:
- call[outevent] += call_partial
- except UndefinedEvent:
- call[outevent] = call_partial
- partial += call_partial
- partials[function] = partial
- try:
- function[outevent] += partial
- except UndefinedEvent:
- function[outevent] = partial
- return partials[function]
-
- def aggregate(self, event):
- """Aggregate an event for the whole profile."""
-
- total = event.null()
- for function in self.functions.itervalues():
- try:
- total = event.aggregate(total, function[event])
- except UndefinedEvent:
- return
- self[event] = total
-
- def ratio(self, outevent, inevent):
- assert outevent not in self
- assert inevent in self
- for function in self.functions.itervalues():
- assert outevent not in function
- assert inevent in function
- function[outevent] = ratio(function[inevent], self[inevent])
- for call in function.calls.itervalues():
- assert outevent not in call
- if inevent in call:
- call[outevent] = ratio(call[inevent], self[inevent])
- self[outevent] = 1.0
-
- def prune(self, node_thres, edge_thres):
- """Prune the profile"""
-
- # compute the prune ratios
- for function in self.functions.itervalues():
- try:
- function[PRUNE_RATIO] = function[TOTAL_TIME_RATIO]
- except UndefinedEvent:
- pass
-
- for call in function.calls.itervalues():
- callee = self.functions[call.callee_id]
-
- if TOTAL_TIME_RATIO in call:
- # handle exact cases first
- call[PRUNE_RATIO] = call[TOTAL_TIME_RATIO]
- else:
- try:
- # make a safe estimate
- call[PRUNE_RATIO] = min(function[TOTAL_TIME_RATIO], callee[TOTAL_TIME_RATIO])
- except UndefinedEvent:
- pass
-
- # prune the nodes
- for function_id in self.functions.keys():
- function = self.functions[function_id]
- try:
- if function[PRUNE_RATIO] < node_thres:
- del self.functions[function_id]
- except UndefinedEvent:
- pass
-
- # prune the egdes
- for function in self.functions.itervalues():
- for callee_id in function.calls.keys():
- call = function.calls[callee_id]
- try:
- if callee_id not in self.functions or call[PRUNE_RATIO] < edge_thres:
- del function.calls[callee_id]
- except UndefinedEvent:
- pass
-
- def dump(self):
- for function in self.functions.itervalues():
- sys.stderr.write('Function %s:\n' % (function.name,))
- self._dump_events(function.events)
- for call in function.calls.itervalues():
- callee = self.functions[call.callee_id]
- sys.stderr.write(' Call %s:\n' % (callee.name,))
- self._dump_events(call.events)
-
- def _dump_events(self, events):
- for event, value in events.iteritems():
- sys.stderr.write(' %s: %s\n' % (event.name, event.format(value)))
-
-
-class Struct:
- """Masquerade a dictionary with a structure-like behavior."""
-
- def __init__(self, attrs = None):
- if attrs is None:
- attrs = {}
- self.__dict__['_attrs'] = attrs
-
- def __getattr__(self, name):
- try:
- return self._attrs[name]
- except KeyError:
- raise AttributeError(name)
-
- def __setattr__(self, name, value):
- self._attrs[name] = value
-
- def __str__(self):
- return str(self._attrs)
-
- def __repr__(self):
- return repr(self._attrs)
-
-
-class ParseError(Exception):
- """Raised when parsing to signal mismatches."""
-
- def __init__(self, msg, line):
- self.msg = msg
- # TODO: store more source line information
- self.line = line
-
- def __str__(self):
- return '%s: %r' % (self.msg, self.line)
-
-
-class Parser:
- """Parser interface."""
-
- def __init__(self):
- pass
-
- def parse(self):
- raise NotImplementedError
-
-
-class LineParser(Parser):
- """Base class for parsers that read line-based formats."""
-
- def __init__(self, file):
- Parser.__init__(self)
- self._file = file
- self.__line = None
- self.__eof = False
-
- def readline(self):
- line = self._file.readline()
- if not line:
- self.__line = ''
- self.__eof = True
- self.__line = line.rstrip('\r\n')
-
- def lookahead(self):
- assert self.__line is not None
- return self.__line
-
- def consume(self):
- assert self.__line is not None
- line = self.__line
- self.readline()
- return line
-
- def eof(self):
- assert self.__line is not None
- return self.__eof
-
-
-class GprofParser(Parser):
- """Parser for GNU gprof output.
-
- See also:
- - Chapter "Interpreting gprof's Output" from the GNU gprof manual
- http://sourceware.org/binutils/docs-2.18/gprof/Call-Graph.html#Call-Graph
- - File "cg_print.c" from the GNU gprof source code
- http://sourceware.org/cgi-bin/cvsweb.cgi/~checkout~/src/gprof/cg_print.c?rev=1.12&cvsroot=src
- """
-
- def __init__(self, fp):
- Parser.__init__(self)
- self.fp = fp
- self.functions = {}
- self.cycles = {}
-
- def readline(self):
- line = self.fp.readline()
- if not line:
- sys.stderr.write('error: unexpected end of file\n')
- sys.exit(1)
- line = line.rstrip('\r\n')
- return line
-
- _int_re = re.compile(r'^\d+$')
- _float_re = re.compile(r'^\d+\.\d+$')
-
- def translate(self, mo):
- """Extract a structure from a match object, while translating the types in the process."""
- attrs = {}
- groupdict = mo.groupdict()
- for name, value in groupdict.iteritems():
- if value is None:
- value = None
- elif self._int_re.match(value):
- value = int(value)
- elif self._float_re.match(value):
- value = float(value)
- attrs[name] = (value)
- return Struct(attrs)
-
- _cg_header_re = re.compile(
- # original gprof header
- r'^\s+called/total\s+parents\s*$|' +
- r'^index\s+%time\s+self\s+descendents\s+called\+self\s+name\s+index\s*$|' +
- r'^\s+called/total\s+children\s*$|' +
- # GNU gprof header
- r'^index\s+%\s+time\s+self\s+children\s+called\s+name\s*$'
- )
-
- _cg_ignore_re = re.compile(
- # spontaneous
- r'^\s+\s*$|'
- # internal calls (such as "mcount")
- r'^.*\((\d+)\)$'
- )
-
- _cg_primary_re = re.compile(
- r'^\[(?P\d+)\]' +
- r'\s+(?P\d+\.\d+)' +
- r'\s+(?P\d+\.\d+)' +
- r'\s+(?P\d+\.\d+)' +
- r'\s+(?:(?P\d+)(?:\+(?P\d+))?)?' +
- r'\s+(?P\S.*?)' +
- r'(?:\s+\d+)>)?' +
- r'\s\[(\d+)\]$'
- )
-
- _cg_parent_re = re.compile(
- r'^\s+(?P\d+\.\d+)?' +
- r'\s+(?P\d+\.\d+)?' +
- r'\s+(?P\d+)(?:/(?P\d+))?' +
- r'\s+(?P\S.*?)' +
- r'(?:\s+\d+)>)?' +
- r'\s\[(?P\d+)\]$'
- )
-
- _cg_child_re = _cg_parent_re
-
- _cg_cycle_header_re = re.compile(
- r'^\[(?P\d+)\]' +
- r'\s+(?P\d+\.\d+)' +
- r'\s+(?P\d+\.\d+)' +
- r'\s+(?P\d+\.\d+)' +
- r'\s+(?:(?P\d+)(?:\+(?P\d+))?)?' +
- r'\s+\d+)\sas\sa\swhole>' +
- r'\s\[(\d+)\]$'
- )
-
- _cg_cycle_member_re = re.compile(
- r'^\s+(?P\d+\.\d+)?' +
- r'\s+(?P\d+\.\d+)?' +
- r'\s+(?P\d+)(?:\+(?P\d+))?' +
- r'\s+(?P\S.*?)' +
- r'(?:\s+\d+)>)?' +
- r'\s\[(?P\d+)\]$'
- )
-
- _cg_sep_re = re.compile(r'^--+$')
-
- def parse_function_entry(self, lines):
- parents = []
- children = []
-
- while True:
- if not lines:
- sys.stderr.write('warning: unexpected end of entry\n')
- line = lines.pop(0)
- if line.startswith('['):
- break
-
- # read function parent line
- mo = self._cg_parent_re.match(line)
- if not mo:
- if self._cg_ignore_re.match(line):
- continue
- sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
- else:
- parent = self.translate(mo)
- parents.append(parent)
-
- # read primary line
- mo = self._cg_primary_re.match(line)
- if not mo:
- sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
- return
- else:
- function = self.translate(mo)
-
- while lines:
- line = lines.pop(0)
-
- # read function subroutine line
- mo = self._cg_child_re.match(line)
- if not mo:
- if self._cg_ignore_re.match(line):
- continue
- sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
- else:
- child = self.translate(mo)
- children.append(child)
-
- function.parents = parents
- function.children = children
-
- self.functions[function.index] = function
-
- def parse_cycle_entry(self, lines):
-
- # read cycle header line
- line = lines[0]
- mo = self._cg_cycle_header_re.match(line)
- if not mo:
- sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
- return
- cycle = self.translate(mo)
-
- # read cycle member lines
- cycle.functions = []
- for line in lines[1:]:
- mo = self._cg_cycle_member_re.match(line)
- if not mo:
- sys.stderr.write('warning: unrecognized call graph entry: %r\n' % line)
- continue
- call = self.translate(mo)
- cycle.functions.append(call)
-
- self.cycles[cycle.cycle] = cycle
-
- def parse_cg_entry(self, lines):
- if lines[0].startswith("["):
- self.parse_cycle_entry(lines)
- else:
- self.parse_function_entry(lines)
-
- def parse_cg(self):
- """Parse the call graph."""
-
- # skip call graph header
- while not self._cg_header_re.match(self.readline()):
- pass
- line = self.readline()
- while self._cg_header_re.match(line):
- line = self.readline()
-
- # process call graph entries
- entry_lines = []
- while line != '\014': # form feed
- if line and not line.isspace():
- if self._cg_sep_re.match(line):
- self.parse_cg_entry(entry_lines)
- entry_lines = []
- else:
- entry_lines.append(line)
- line = self.readline()
-
- def parse(self):
- self.parse_cg()
- self.fp.close()
-
- profile = Profile()
- profile[TIME] = 0.0
-
- cycles = {}
- for index in self.cycles.iterkeys():
- cycles[index] = Cycle()
-
- for entry in self.functions.itervalues():
- # populate the function
- function = Function(entry.index, entry.name)
- function[TIME] = entry.self
- if entry.called is not None:
- function[CALLS] = entry.called
- if entry.called_self is not None:
- call = Call(entry.index)
- call[CALLS] = entry.called_self
- function[CALLS] += entry.called_self
-
- # populate the function calls
- for child in entry.children:
- call = Call(child.index)
-
- assert child.called is not None
- call[CALLS] = child.called
-
- if child.index not in self.functions:
- # NOTE: functions that were never called but were discovered by gprof's
- # static call graph analysis dont have a call graph entry so we need
- # to add them here
- missing = Function(child.index, child.name)
- function[TIME] = 0.0
- function[CALLS] = 0
- profile.add_function(missing)
-
- function.add_call(call)
-
- profile.add_function(function)
-
- if entry.cycle is not None:
- cycles[entry.cycle].add_function(function)
-
- profile[TIME] = profile[TIME] + function[TIME]
-
- for cycle in cycles.itervalues():
- profile.add_cycle(cycle)
-
- # Compute derived events
- profile.validate()
- profile.ratio(TIME_RATIO, TIME)
- profile.call_ratios(CALLS)
- profile.integrate(TOTAL_TIME, TIME)
- profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
-
- return profile
-
-
-class OprofileParser(LineParser):
- """Parser for oprofile callgraph output.
-
- See also:
- - http://oprofile.sourceforge.net/doc/opreport.html#opreport-callgraph
- """
-
- _fields_re = {
- 'samples': r'(?P\d+)',
- '%': r'(?P\S+)',
- 'linenr info': r'(?P\(no location information\)|\S+:\d+)',
- 'image name': r'(?P\S+(?:\s\(tgid:[^)]*\))?)',
- 'app name': r'(?P\S+)',
- 'symbol name': r'(?P\(no symbols\)|.+?)',
- }
-
- def __init__(self, infile):
- LineParser.__init__(self, infile)
- self.entries = {}
- self.entry_re = None
-
- def add_entry(self, callers, function, callees):
- try:
- entry = self.entries[function.id]
- except KeyError:
- self.entries[function.id] = (callers, function, callees)
- else:
- callers_total, function_total, callees_total = entry
- self.update_subentries_dict(callers_total, callers)
- function_total.samples += function.samples
- self.update_subentries_dict(callees_total, callees)
-
- def update_subentries_dict(self, totals, partials):
- for partial in partials.itervalues():
- try:
- total = totals[partial.id]
- except KeyError:
- totals[partial.id] = partial
- else:
- total.samples += partial.samples
-
- def parse(self):
- # read lookahead
- self.readline()
-
- self.parse_header()
- while self.lookahead():
- self.parse_entry()
-
- profile = Profile()
-
- reverse_call_samples = {}
-
- # populate the profile
- profile[SAMPLES] = 0
- for _callers, _function, _callees in self.entries.itervalues():
- function = Function(_function.id, _function.name)
- function[SAMPLES] = _function.samples
- profile.add_function(function)
- profile[SAMPLES] += _function.samples
-
- if _function.application:
- function[PROCESS] = os.path.basename(_function.application)
- if _function.image:
- function[MODULE] = os.path.basename(_function.image)
-
- total_callee_samples = 0
- for _callee in _callees.itervalues():
- total_callee_samples += _callee.samples
-
- for _callee in _callees.itervalues():
- if not _callee.self:
- call = Call(_callee.id)
- call[SAMPLES] = _callee.samples
- function.add_call(call)
-
- # compute derived data
- profile.validate()
- profile.find_cycles()
- profile.ratio(TIME_RATIO, SAMPLES)
- profile.call_ratios(SAMPLES)
- profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
-
- return profile
-
- def parse_header(self):
- while not self.match_header():
- self.consume()
- line = self.lookahead()
- fields = re.split(r'\s\s+', line)
- entry_re = r'^\s*' + r'\s+'.join([self._fields_re[field] for field in fields]) + r'(?P\s+\[self\])?$'
- self.entry_re = re.compile(entry_re)
- self.skip_separator()
-
- def parse_entry(self):
- callers = self.parse_subentries()
- if self.match_primary():
- function = self.parse_subentry()
- if function is not None:
- callees = self.parse_subentries()
- self.add_entry(callers, function, callees)
- self.skip_separator()
-
- def parse_subentries(self):
- subentries = {}
- while self.match_secondary():
- subentry = self.parse_subentry()
- subentries[subentry.id] = subentry
- return subentries
-
- def parse_subentry(self):
- entry = Struct()
- line = self.consume()
- mo = self.entry_re.match(line)
- if not mo:
- raise ParseError('failed to parse', line)
- fields = mo.groupdict()
- entry.samples = int(fields.get('samples', 0))
- entry.percentage = float(fields.get('percentage', 0.0))
- if 'source' in fields and fields['source'] != '(no location information)':
- source = fields['source']
- filename, lineno = source.split(':')
- entry.filename = filename
- entry.lineno = int(lineno)
- else:
- source = ''
- entry.filename = None
- entry.lineno = None
- entry.image = fields.get('image', '')
- entry.application = fields.get('application', '')
- if 'symbol' in fields and fields['symbol'] != '(no symbols)':
- entry.symbol = fields['symbol']
- else:
- entry.symbol = ''
- if entry.symbol.startswith('"') and entry.symbol.endswith('"'):
- entry.symbol = entry.symbol[1:-1]
- entry.id = ':'.join((entry.application, entry.image, source, entry.symbol))
- entry.self = fields.get('self', None) != None
- if entry.self:
- entry.id += ':self'
- if entry.symbol:
- entry.name = entry.symbol
- else:
- entry.name = entry.image
- return entry
-
- def skip_separator(self):
- while not self.match_separator():
- self.consume()
- self.consume()
-
- def match_header(self):
- line = self.lookahead()
- return line.startswith('samples')
-
- def match_separator(self):
- line = self.lookahead()
- return line == '-'*len(line)
-
- def match_primary(self):
- line = self.lookahead()
- return not line[:1].isspace()
-
- def match_secondary(self):
- line = self.lookahead()
- return line[:1].isspace()
-
-
-class SharkParser(LineParser):
- """Parser for MacOSX Shark output.
-
- Author: tom@dbservice.com
- """
-
- def __init__(self, infile):
- LineParser.__init__(self, infile)
- self.stack = []
- self.entries = {}
-
- def add_entry(self, function):
- try:
- entry = self.entries[function.id]
- except KeyError:
- self.entries[function.id] = (function, { })
- else:
- function_total, callees_total = entry
- function_total.samples += function.samples
-
- def add_callee(self, function, callee):
- func, callees = self.entries[function.id]
- try:
- entry = callees[callee.id]
- except KeyError:
- callees[callee.id] = callee
- else:
- entry.samples += callee.samples
-
- def parse(self):
- self.readline()
- self.readline()
- self.readline()
- self.readline()
-
- match = re.compile(r'(?P[|+ ]*)(?P\d+), (?P[^,]+), (?P.*)')
-
- while self.lookahead():
- line = self.consume()
- mo = match.match(line)
- if not mo:
- raise ParseError('failed to parse', line)
-
- fields = mo.groupdict()
- prefix = len(fields.get('prefix', 0)) / 2 - 1
-
- symbol = str(fields.get('symbol', 0))
- image = str(fields.get('image', 0))
-
- entry = Struct()
- entry.id = ':'.join([symbol, image])
- entry.samples = int(fields.get('samples', 0))
-
- entry.name = symbol
- entry.image = image
-
- # adjust the callstack
- if prefix < len(self.stack):
- del self.stack[prefix:]
-
- if prefix == len(self.stack):
- self.stack.append(entry)
-
- # if the callstack has had an entry, it's this functions caller
- if prefix > 0:
- self.add_callee(self.stack[prefix - 1], entry)
-
- self.add_entry(entry)
-
- profile = Profile()
- profile[SAMPLES] = 0
- for _function, _callees in self.entries.itervalues():
- function = Function(_function.id, _function.name)
- function[SAMPLES] = _function.samples
- profile.add_function(function)
- profile[SAMPLES] += _function.samples
-
- if _function.image:
- function[MODULE] = os.path.basename(_function.image)
-
- for _callee in _callees.itervalues():
- call = Call(_callee.id)
- call[SAMPLES] = _callee.samples
- function.add_call(call)
-
- # compute derived data
- profile.validate()
- profile.find_cycles()
- profile.ratio(TIME_RATIO, SAMPLES)
- profile.call_ratios(SAMPLES)
- profile.integrate(TOTAL_TIME_RATIO, TIME_RATIO)
-
- return profile
-
-
-class PstatsParser:
- """Parser python profiling statistics saved with te pstats module."""
-
- def __init__(self, *filename):
- import pstats
- self.stats = pstats.Stats(*filename)
- self.profile = Profile()
- self.function_ids = {}
-
- def get_function_name(self, (filename, line, name)):
- module = os.path.splitext(filename)[0]
- module = os.path.basename(module)
- return "%s:%d:%s" % (module, line, name)
-
- def get_function(self, key):
- try:
- id = self.function_ids[key]
- except KeyError:
- id = len(self.function_ids)
- name = self.get_function_name(key)
- function = Function(id, name)
- self.profile.functions[id] = function
- self.function_ids[key] = id
- else:
- function = self.profile.functions[id]
- return function
-
- def parse(self):
- self.profile[TIME] = 0.0
- self.profile[TOTAL_TIME] = self.stats.total_tt
- for fn, (cc, nc, tt, ct, callers) in self.stats.stats.iteritems():
- callee = self.get_function(fn)
- callee[CALLS] = nc
- callee[TOTAL_TIME] = ct
- callee[TIME] = tt
- self.profile[TIME] += tt
- self.profile[TOTAL_TIME] = max(self.profile[TOTAL_TIME], ct)
- for fn, value in callers.iteritems():
- caller = self.get_function(fn)
- call = Call(callee.id)
- if isinstance(value, tuple):
- for i in xrange(0, len(value), 4):
- nc, cc, tt, ct = value[i:i+4]
- if CALLS in call:
- call[CALLS] += cc
- else:
- call[CALLS] = cc
-
- if TOTAL_TIME in call:
- call[TOTAL_TIME] += ct
- else:
- call[TOTAL_TIME] = ct
-
- else:
- call[CALLS] = value
- call[TOTAL_TIME] = ratio(value, nc)*ct
-
- caller.add_call(call)
- #self.stats.print_stats()
- #self.stats.print_callees()
-
- # Compute derived events
- self.profile.validate()
- self.profile.ratio(TIME_RATIO, TIME)
- self.profile.ratio(TOTAL_TIME_RATIO, TOTAL_TIME)
-
- return self.profile
-
-
-class Theme:
-
- def __init__(self,
- bgcolor = (0.0, 0.0, 1.0),
- mincolor = (0.0, 0.0, 0.0),
- maxcolor = (0.0, 0.0, 1.0),
- fontname = "Arial",
- minfontsize = 10.0,
- maxfontsize = 10.0,
- minpenwidth = 0.5,
- maxpenwidth = 4.0,
- gamma = 2.2):
- self.bgcolor = bgcolor
- self.mincolor = mincolor
- self.maxcolor = maxcolor
- self.fontname = fontname
- self.minfontsize = minfontsize
- self.maxfontsize = maxfontsize
- self.minpenwidth = minpenwidth
- self.maxpenwidth = maxpenwidth
- self.gamma = gamma
-
- def graph_bgcolor(self):
- return self.hsl_to_rgb(*self.bgcolor)
-
- def graph_fontname(self):
- return self.fontname
-
- def graph_fontsize(self):
- return self.minfontsize
-
- def node_bgcolor(self, weight):
- return self.color(weight)
-
- def node_fgcolor(self, weight):
- return self.graph_bgcolor()
-
- def node_fontsize(self, weight):
- return self.fontsize(weight)
-
- def edge_color(self, weight):
- return self.color(weight)
-
- def edge_fontsize(self, weight):
- return self.fontsize(weight)
-
- def edge_penwidth(self, weight):
- return max(weight*self.maxpenwidth, self.minpenwidth)
-
- def edge_arrowsize(self, weight):
- return 0.5 * math.sqrt(self.edge_penwidth(weight))
-
- def fontsize(self, weight):
- return max(weight**2 * self.maxfontsize, self.minfontsize)
-
- def color(self, weight):
- weight = min(max(weight, 0.0), 1.0)
-
- hmin, smin, lmin = self.mincolor
- hmax, smax, lmax = self.maxcolor
-
- h = hmin + weight*(hmax - hmin)
- s = smin + weight*(smax - smin)
- l = lmin + weight*(lmax - lmin)
-
- return self.hsl_to_rgb(h, s, l)
-
- def hsl_to_rgb(self, h, s, l):
- """Convert a color from HSL color-model to RGB.
-
- See also:
- - http://www.w3.org/TR/css3-color/#hsl-color
- """
-
- h = h % 1.0
- s = min(max(s, 0.0), 1.0)
- l = min(max(l, 0.0), 1.0)
-
- if l <= 0.5:
- m2 = l*(s + 1.0)
- else:
- m2 = l + s - l*s
- m1 = l*2.0 - m2
- r = self._hue_to_rgb(m1, m2, h + 1.0/3.0)
- g = self._hue_to_rgb(m1, m2, h)
- b = self._hue_to_rgb(m1, m2, h - 1.0/3.0)
-
- # Apply gamma correction
- r **= self.gamma
- g **= self.gamma
- b **= self.gamma
-
- return (r, g, b)
-
- def _hue_to_rgb(self, m1, m2, h):
- if h < 0.0:
- h += 1.0
- elif h > 1.0:
- h -= 1.0
- if h*6 < 1.0:
- return m1 + (m2 - m1)*h*6.0
- elif h*2 < 1.0:
- return m2
- elif h*3 < 2.0:
- return m1 + (m2 - m1)*(2.0/3.0 - h)*6.0
- else:
- return m1
-
-
-TEMPERATURE_COLORMAP = Theme(
- mincolor = (2.0/3.0, 0.80, 0.25), # dark blue
- maxcolor = (0.0, 1.0, 0.5), # satured red
- gamma = 1.0
-)
-
-PINK_COLORMAP = Theme(
- mincolor = (0.0, 1.0, 0.90), # pink
- maxcolor = (0.0, 1.0, 0.5), # satured red
-)
-
-GRAY_COLORMAP = Theme(
- mincolor = (0.0, 0.0, 0.85), # light gray
- maxcolor = (0.0, 0.0, 0.0), # black
-)
-
-BW_COLORMAP = Theme(
- minfontsize = 8.0,
- maxfontsize = 24.0,
- mincolor = (0.0, 0.0, 0.0), # black
- maxcolor = (0.0, 0.0, 0.0), # black
- minpenwidth = 0.1,
- maxpenwidth = 8.0,
-)
-
-
-class DotWriter:
- """Writer for the DOT language.
-
- See also:
- - "The DOT Language" specification
- http://www.graphviz.org/doc/info/lang.html
- """
-
- def __init__(self, fp):
- self.fp = fp
-
- def graph(self, profile, theme):
- self.begin_graph()
-
- fontname = theme.graph_fontname()
-
- self.attr('graph', fontname=fontname, ranksep=0.25, nodesep=0.125)
- self.attr('node', fontname=fontname, shape="box", style="filled,rounded", fontcolor="white", width=0, height=0)
- self.attr('edge', fontname=fontname)
-
- for function in profile.functions.itervalues():
- labels = []
- for event in PROCESS, MODULE:
- if event in function.events:
- label = event.format(function[event])
- labels.append(label)
- labels.append(function.name)
- for event in TOTAL_TIME_RATIO, TIME_RATIO, CALLS:
- if event in function.events:
- label = event.format(function[event])
- labels.append(label)
-
- try:
- weight = function[PRUNE_RATIO]
- except UndefinedEvent:
- weight = 0.0
-
- label = '\n'.join(labels)
- self.node(function.id,
- label = label,
- color = self.color(theme.node_bgcolor(weight)),
- fontcolor = self.color(theme.node_fgcolor(weight)),
- fontsize = "%.2f" % theme.node_fontsize(weight),
- )
-
- for call in function.calls.itervalues():
- callee = profile.functions[call.callee_id]
-
- labels = []
- for event in TOTAL_TIME_RATIO, CALLS:
- if event in call.events:
- label = event.format(call[event])
- labels.append(label)
-
- try:
- weight = call[PRUNE_RATIO]
- except UndefinedEvent:
- try:
- weight = callee[PRUNE_RATIO]
- except UndefinedEvent:
- weight = 0.0
-
- label = '\n'.join(labels)
-
- self.edge(function.id, call.callee_id,
- label = label,
- color = self.color(theme.edge_color(weight)),
- fontcolor = self.color(theme.edge_color(weight)),
- fontsize = "%.2f" % theme.edge_fontsize(weight),
- penwidth = "%.2f" % theme.edge_penwidth(weight),
- labeldistance = "%.2f" % theme.edge_penwidth(weight),
- arrowsize = "%.2f" % theme.edge_arrowsize(weight),
- )
-
- self.end_graph()
-
- def begin_graph(self):
- self.write('digraph {\n')
-
- def end_graph(self):
- self.write('}\n')
-
- def attr(self, what, **attrs):
- self.write("\t")
- self.write(what)
- self.attr_list(attrs)
- self.write(";\n")
-
- def node(self, node, **attrs):
- self.write("\t")
- self.id(node)
- self.attr_list(attrs)
- self.write(";\n")
-
- def edge(self, src, dst, **attrs):
- self.write("\t")
- self.id(src)
- self.write(" -> ")
- self.id(dst)
- self.attr_list(attrs)
- self.write(";\n")
-
- def attr_list(self, attrs):
- if not attrs:
- return
- self.write(' [')
- first = True
- for name, value in attrs.iteritems():
- if first:
- first = False
- else:
- self.write(", ")
- self.id(name)
- self.write('=')
- self.id(value)
- self.write(']')
-
- def id(self, id):
- if isinstance(id, (int, float)):
- s = str(id)
- elif isinstance(id, str):
- if id.isalnum():
- s = id
- else:
- s = self.escape(id)
- else:
- raise TypeError
- self.write(s)
-
- def color(self, (r, g, b)):
-
- def float2int(f):
- if f <= 0.0:
- return 0
- if f >= 1.0:
- return 255
- return int(255.0*f + 0.5)
-
- return "#" + "".join(["%02x" % float2int(c) for c in (r, g, b)])
-
- def escape(self, s):
- s = s.encode('utf-8')
- s = s.replace('\\', r'\\')
- s = s.replace('\n', r'\n')
- s = s.replace('\t', r'\t')
- s = s.replace('"', r'\"')
- return '"' + s + '"'
-
- def write(self, s):
- self.fp.write(s)
-
-
-class Main:
- """Main program."""
-
- themes = {
- "color": TEMPERATURE_COLORMAP,
- "pink": PINK_COLORMAP,
- "gray": GRAY_COLORMAP,
- "bw": BW_COLORMAP,
- }
-
- def main(self):
- """Main program."""
-
- parser = optparse.OptionParser(
- usage="\n\t%prog [options] [file] ...",
- version="%%prog %s" % __version__)
- parser.add_option(
- '-o', '--output', metavar='FILE',
- type="string", dest="output",
- help="output filename [stdout]")
- parser.add_option(
- '-n', '--node-thres', metavar='PERCENTAGE',
- type="float", dest="node_thres", default=0.5,
- help="eliminate nodes below this threshold [default: %default]")
- parser.add_option(
- '-e', '--edge-thres', metavar='PERCENTAGE',
- type="float", dest="edge_thres", default=0.1,
- help="eliminate edges below this threshold [default: %default]")
- parser.add_option(
- '-f', '--format',
- type="choice", choices=('prof', 'oprofile', 'pstats', 'shark'),
- dest="format", default="prof",
- help="profile format: prof, oprofile, or pstats [default: %default]")
- parser.add_option(
- '-c', '--colormap',
- type="choice", choices=('color', 'pink', 'gray', 'bw'),
- dest="theme", default="color",
- help="color map: color, pink, gray, or bw [default: %default]")
- parser.add_option(
- '-s', '--strip',
- action="store_true",
- dest="strip", default=False,
- help="strip function parameters, template parameters, and const modifiers from demangled C++ function names")
- parser.add_option(
- '-w', '--wrap',
- action="store_true",
- dest="wrap", default=False,
- help="wrap function names")
- (self.options, self.args) = parser.parse_args(sys.argv[1:])
-
- if len(self.args) > 1 and self.options.format != 'pstats':
- parser.error('incorrect number of arguments')
-
- try:
- self.theme = self.themes[self.options.theme]
- except KeyError:
- parser.error('invalid colormap \'%s\'' % self.options.theme)
-
- if self.options.format == 'prof':
- if not self.args:
- fp = sys.stdin
- else:
- fp = open(self.args[0], 'rt')
- parser = GprofParser(fp)
- elif self.options.format == 'oprofile':
- if not self.args:
- fp = sys.stdin
- else:
- fp = open(self.args[0], 'rt')
- parser = OprofileParser(fp)
- elif self.options.format == 'pstats':
- if not self.args:
- parser.error('at least a file must be specified for pstats input')
- parser = PstatsParser(*self.args)
- elif self.options.format == 'shark':
- if not self.args:
- fp = sys.stdin
- else:
- fp = open(self.args[0], 'rt')
- parser = SharkParser(fp)
- else:
- parser.error('invalid format \'%s\'' % self.options.format)
-
- self.profile = parser.parse()
-
- if self.options.output is None:
- self.output = sys.stdout
- else:
- self.output = open(self.options.output, 'wt')
-
- self.write_graph()
-
- _parenthesis_re = re.compile(r'\([^()]*\)')
- _angles_re = re.compile(r'<[^<>]*>')
- _const_re = re.compile(r'\s+const$')
-
- def strip_function_name(self, name):
- """Remove extraneous information from C++ demangled function names."""
-
- # Strip function parameters from name by recursively removing paired parenthesis
- while True:
- name, n = self._parenthesis_re.subn('', name)
- if not n:
- break
-
- # Strip const qualifier
- name = self._const_re.sub('', name)
-
- # Strip template parameters from name by recursively removing paired angles
- while True:
- name, n = self._angles_re.subn('', name)
- if not n:
- break
-
- return name
-
- def wrap_function_name(self, name):
- """Split the function name on multiple lines."""
-
- if len(name) > 32:
- ratio = 2.0/3.0
- height = max(int(len(name)/(1.0 - ratio) + 0.5), 1)
- width = max(len(name)/height, 32)
- # TODO: break lines in symbols
- name = textwrap.fill(name, width, break_long_words=False)
-
- # Take away spaces
- name = name.replace(", ", ",")
- name = name.replace("> >", ">>")
- name = name.replace("> >", ">>") # catch consecutive
-
- return name
-
- def compress_function_name(self, name):
- """Compress function name according to the user preferences."""
-
- if self.options.strip:
- name = self.strip_function_name(name)
-
- if self.options.wrap:
- name = self.wrap_function_name(name)
-
- # TODO: merge functions with same resulting name
-
- return name
-
- def write_graph(self):
- dot = DotWriter(self.output)
- profile = self.profile
- profile.prune(self.options.node_thres/100.0, self.options.edge_thres/100.0)
-
- for function in profile.functions.itervalues():
- function.name = self.compress_function_name(function.name)
-
- dot.graph(profile, self.theme)
-
-
-if __name__ == '__main__':
- Main().main()
diff --git a/libs/thetvdb/tests/runtests.py b/libs/thetvdb/tests/runtests.py
deleted file mode 100755
index ebb73d9c..00000000
--- a/libs/thetvdb/tests/runtests.py
+++ /dev/null
@@ -1,28 +0,0 @@
-#!/usr/bin/env python
-#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())
- )
diff --git a/libs/thetvdb/tests/test_tvdb_api.py b/libs/thetvdb/tests/test_tvdb_api.py
deleted file mode 100644
index 0947461e..00000000
--- a/libs/thetvdb/tests/test_tvdb_api.py
+++ /dev/null
@@ -1,526 +0,0 @@
-#!/usr/bin/env python
-#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
-import sys
-import datetime
-import unittest
-
-# Force parent directory onto path
-sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
-
-import tvdb_api
-import tvdb_ui
-from tvdb_api import (tvdb_shownotfound, tvdb_seasonnotfound,
-tvdb_episodenotfound, tvdb_attributenotfound)
-
-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
- )
-
-
-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']),
- ""
- )
- def test_repr_season(self):
- """Check repr() of Season
- """
- self.assertEquals(
- repr(self.t['CNNNN'][1]),
- ""
- )
- def test_repr_episode(self):
- """Check repr() of Episode
- """
- self.assertEquals(
- repr(self.t['CNNNN'][1][1]),
- ""
- )
- 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_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)
-
-
-if __name__ == '__main__':
- runner = unittest.TextTestRunner(verbosity = 2)
- unittest.main(testRunner = runner)
diff --git a/libs/thetvdb/tvdb_api.py b/libs/thetvdb/tvdb_api.py
deleted file mode 100644
index 4bfe78a2..00000000
--- a/libs/thetvdb/tvdb_api.py
+++ /dev/null
@@ -1,874 +0,0 @@
-#!/usr/bin/env python
-#encoding:utf-8
-#author:dbr/Ben
-#project:tvdb_api
-#repository:http://github.com/dbr/tvdb_api
-#license:unlicense (http://unlicense.org/)
-
-"""Simple-to-use Python interface to The TVDB's API (thetvdb.com)
-
-Example usage:
-
->>> from tvdb_api import Tvdb
->>> t = Tvdb()
->>> t['Lost'][4][11]['episodename']
-u'Cabin Fever'
-"""
-__author__ = "dbr/Ben"
-__version__ = "1.8.2"
-
-import os
-import time
-import urllib
-import urllib2
-import getpass
-import StringIO
-import tempfile
-import warnings
-import logging
-import datetime
-import zipfile
-
-try:
- import xml.etree.cElementTree as ElementTree
-except ImportError:
- import xml.etree.ElementTree as ElementTree
-
-try:
- import gzip
-except ImportError:
- gzip = None
-
-
-from tvdb_cache import CacheHandler
-
-from tvdb_ui import BaseUI, ConsoleUI
-from tvdb_exceptions import (tvdb_error, tvdb_userabort, tvdb_shownotfound,
- tvdb_seasonnotfound, tvdb_episodenotfound, tvdb_attributenotfound)
-
-lastTimeout = None
-
-def log():
- return logging.getLogger("tvdb_api")
-
-
-class ShowContainer(dict):
- """Simple dict that holds a series of Show instances
- """
-
- def __init__(self):
- self._stack = []
- self._lastgc = time.time()
-
- def __setitem__(self, key, value):
- self._stack.append(key)
-
- #keep only the 100th latest results
- if time.time() - self._lastgc > 20:
- tbd = self._stack[:-100]
- i = 0
- for o in tbd:
- del self[o]
- del self._stack[i]
- i += 1
-
- _lastgc = time.time()
- del tbd
-
- super(ShowContainer, self).__setitem__(key, value)
-
-
-class Show(dict):
- """Holds a dict of seasons, and show data.
- """
- def __init__(self):
- dict.__init__(self)
- self.data = {}
-
- def __repr__(self):
- return "" % (
- self.data.get(u'seriesname', 'instance'),
- len(self)
- )
-
- def __getitem__(self, key):
- if key in self:
- # Key is an episode, return it
- return dict.__getitem__(self, key)
-
- if key in self.data:
- # Non-numeric request is for show-data
- return dict.__getitem__(self.data, key)
-
- # Data wasn't found, raise appropriate error
- if isinstance(key, int) or key.isdigit():
- # Episode number x was not found
- raise tvdb_seasonnotfound("Could not find season %s" % (repr(key)))
- else:
- # If it's not numeric, it must be an attribute name, which
- # doesn't exist, so attribute error.
- raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
-
- def airedOn(self, date):
- ret = self.search(str(date), 'firstaired')
- if len(ret) == 0:
- raise tvdb_episodenotfound("Could not find any episodes that aired on %s" % date)
- return ret
-
- def search(self, term = None, key = None):
- """
- Search all episodes in show. Can search all data, or a specific key (for
- example, episodename)
-
- Always returns an array (can be empty). First index contains the first
- match, and so on.
-
- Each array index is an Episode() instance, so doing
- search_results[0]['episodename'] will retrieve the episode name of the
- first match.
-
- Search terms are converted to lower case (unicode) strings.
-
- # Examples
-
- These examples assume t is an instance of Tvdb():
-
- >>> t = Tvdb()
- >>>
-
- To search for all episodes of Scrubs with a bit of data
- containing "my first day":
-
- >>> t['Scrubs'].search("my first day")
- []
- >>>
-
- Search for "My Name Is Earl" episode named "Faked His Own Death":
-
- >>> t['My Name Is Earl'].search('Faked His Own Death', key = 'episodename')
- []
- >>>
-
- To search Scrubs for all episodes with "mentor" in the episode name:
-
- >>> t['scrubs'].search('mentor', key = 'episodename')
- [, ]
- >>>
-
- # Using search results
-
- >>> results = t['Scrubs'].search("my first")
- >>> print results[0]['episodename']
- My First Day
- >>> for x in results: print x['episodename']
- My First Day
- My First Step
- My First Kill
- >>>
- """
- results = []
- for cur_season in self.values():
- searchresult = cur_season.search(term = term, key = key)
- if len(searchresult) != 0:
- results.extend(searchresult)
-
- return results
-
-
-class Season(dict):
- def __init__(self, show = None):
- """The show attribute points to the parent show
- """
- self.show = show
-
- def __repr__(self):
- return "" % (
- len(self.keys())
- )
-
- def __getitem__(self, episode_number):
- if episode_number not in self:
- raise tvdb_episodenotfound("Could not find episode %s" % (repr(episode_number)))
- else:
- return dict.__getitem__(self, episode_number)
-
- def search(self, term = None, key = None):
- """Search all episodes in season, returns a list of matching Episode
- instances.
-
- >>> t = Tvdb()
- >>> t['scrubs'][1].search('first day')
- []
- >>>
-
- See Show.search documentation for further information on search
- """
- results = []
- for ep in self.values():
- searchresult = ep.search(term = term, key = key)
- if searchresult is not None:
- results.append(
- searchresult
- )
- return results
-
-
-class Episode(dict):
- def __init__(self, season = None):
- """The season attribute points to the parent season
- """
- self.season = season
-
- def __repr__(self):
- seasno = int(self.get(u'seasonnumber', 0))
- epno = int(self.get(u'episodenumber', 0))
- epname = self.get(u'episodename')
- if epname is not None:
- return "" % (seasno, epno, epname)
- else:
- return "" % (seasno, epno)
-
- def __getitem__(self, key):
- try:
- return dict.__getitem__(self, key)
- except KeyError:
- raise tvdb_attributenotfound("Cannot find attribute %s" % (repr(key)))
-
- def search(self, term = None, key = None):
- """Search episode data for term, if it matches, return the Episode (self).
- The key parameter can be used to limit the search to a specific element,
- for example, episodename.
-
- This primarily for use use by Show.search and Season.search. See
- Show.search for further information on search
-
- Simple example:
-
- >>> e = Episode()
- >>> e['episodename'] = "An Example"
- >>> e.search("examp")
-
- >>>
-
- Limiting by key:
-
- >>> e.search("examp", key = "episodename")
-
- >>>
- """
- if term == None:
- raise TypeError("must supply string to search for (contents)")
-
- term = unicode(term).lower()
- for cur_key, cur_value in self.items():
- cur_key, cur_value = unicode(cur_key).lower(), unicode(cur_value).lower()
- if key is not None and cur_key != key:
- # Do not search this key
- continue
- if cur_value.find( unicode(term).lower() ) > -1:
- return self
-
-
-class Actors(list):
- """Holds all Actor instances for a show
- """
- pass
-
-
-class Actor(dict):
- """Represents a single actor. Should contain..
-
- id,
- image,
- name,
- role,
- sortorder
- """
- def __repr__(self):
- return "" % (self.get("name"))
-
-
-class Tvdb:
- """Create easy-to-use interface to name of season/episode name
- >>> t = Tvdb()
- >>> t['Scrubs'][1][24]['episodename']
- u'My Last Day'
- """
- def __init__(self,
- interactive = False,
- select_first = False,
- debug = False,
- cache = True,
- banners = False,
- actors = False,
- custom_ui = None,
- language = None,
- search_all_languages = False,
- apikey = None,
- forceConnect=False,
- useZip=False):
-
- """interactive (True/False):
- When True, uses built-in console UI is used to select the correct show.
- When False, the first search result is used.
-
- select_first (True/False):
- Automatically selects the first series search result (rather
- than showing the user a list of more than one series).
- Is overridden by interactive = False, or specifying a custom_ui
-
- debug (True/False) DEPRECATED:
- Replaced with proper use of logging module. To show debug messages:
-
- >>> import logging
- >>> logging.basicConfig(level = logging.DEBUG)
-
- cache (True/False/str/unicode/urllib2 opener):
- Retrieved XML are persisted to to disc. If true, stores in
- tvdb_api folder under your systems TEMP_DIR, if set to
- str/unicode instance it will use this as the cache
- location. If False, disables caching. Can also be passed
- an arbitrary Python object, which is used as a urllib2
- opener, which should be created by urllib2.build_opener
-
- banners (True/False):
- Retrieves the banners for a show. These are accessed
- via the _banners key of a Show(), for example:
-
- >>> Tvdb(banners=True)['scrubs']['_banners'].keys()
- ['fanart', 'poster', 'series', 'season']
-
- actors (True/False):
- Retrieves a list of the actors for a show. These are accessed
- via the _actors key of a Show(), for example:
-
- >>> t = Tvdb(actors=True)
- >>> t['scrubs']['_actors'][0]['name']
- u'Zach Braff'
-
- custom_ui (tvdb_ui.BaseUI subclass):
- A callable subclass of tvdb_ui.BaseUI (overrides interactive option)
-
- language (2 character language abbreviation):
- The language of the returned data. Is also the language search
- uses. Default is "en" (English). For full list, run..
-
- >>> Tvdb().config['valid_languages'] #doctest: +ELLIPSIS
- ['da', 'fi', 'nl', ...]
-
- search_all_languages (True/False):
- By default, Tvdb will only search in the language specified using
- the language option. When this is True, it will search for the
- show in and language
-
- apikey (str/unicode):
- Override the default thetvdb.com API key. By default it will use
- tvdb_api's own key (fine for small scripts), but you can use your
- own key if desired - this is recommended if you are embedding
- tvdb_api in a larger application)
- See http://thetvdb.com/?tab=apiregister to get your own key
-
- forceConnect (bool):
- If true it will always try to connect to theTVDB.com even if we
- recently timed out. By default it will wait one minute before
- trying again, and any requests within that one minute window will
- return an exception immediately.
-
- useZip (bool):
- Download the zip archive where possibale, instead of the xml.
- This is only used when all episodes are pulled.
- And only the main language xml is used, the actor and banner xml are lost.
- """
-
- global lastTimeout
-
- # if we're given a lastTimeout that is less than 1 min just give up
- if not forceConnect and lastTimeout != None and datetime.datetime.now() - lastTimeout < datetime.timedelta(minutes=1):
- raise tvdb_error("We recently timed out, so giving up early this time")
-
- self.shows = ShowContainer() # Holds all Show classes
- self.corrections = {} # Holds show-name to show_id mapping
-
- self.config = {}
-
- if apikey is not None:
- self.config['apikey'] = apikey
- else:
- self.config['apikey'] = "0629B785CE550C8D" # tvdb_api's API key
-
- self.config['debug_enabled'] = debug # show debugging messages
-
- self.config['custom_ui'] = custom_ui
-
- self.config['interactive'] = interactive # prompt for correct series?
-
- self.config['select_first'] = select_first
-
- self.config['search_all_languages'] = search_all_languages
-
- self.config['useZip'] = useZip
-
-
- if cache is True:
- self.config['cache_enabled'] = True
- self.config['cache_location'] = self._getTempDir()
- self.urlopener = urllib2.build_opener(
- CacheHandler(self.config['cache_location'])
- )
-
- elif cache is False:
- self.config['cache_enabled'] = False
- self.urlopener = urllib2.build_opener() # default opener with no caching
-
- elif isinstance(cache, basestring):
- self.config['cache_enabled'] = True
- self.config['cache_location'] = cache
- self.urlopener = urllib2.build_opener(
- CacheHandler(self.config['cache_location'])
- )
-
- elif isinstance(cache, urllib2.OpenerDirector):
- # If passed something from urllib2.build_opener, use that
- log().debug("Using %r as urlopener" % cache)
- self.config['cache_enabled'] = True
- self.urlopener = cache
-
- else:
- raise ValueError("Invalid value for Cache %r (type was %s)" % (cache, type(cache)))
-
- self.config['banners_enabled'] = banners
- self.config['actors_enabled'] = actors
-
- if self.config['debug_enabled']:
- 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: "
- "import logging; logging.basicConfig(level=logging.DEBUG)")
- logging.basicConfig(level=logging.DEBUG)
-
-
- # 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
- # recommended on http://thetvdb.com/wiki/index.php/API:languages.xml
- self.config['valid_languages'] = [
- "da", "fi", "nl", "de", "it", "es", "fr","pl", "hu","el","tr",
- "ru","he","ja","pt","zh","cs","sl", "hr","ko","en","sv","no"
- ]
-
- # thetvdb.com should be based around numeric language codes,
- # but to link to a series like http://thetvdb.com/?tab=series&id=79349&lid=16
- # requires the language ID, thus this mapping is required (mainly
- # for usage in tvdb_ui - internally tvdb_api will use the language abbreviations)
- self.config['langabbv_to_id'] = {'el': 20, 'en': 7, 'zh': 27,
- 'it': 15, 'cs': 28, 'es': 16, 'ru': 22, 'nl': 13, 'pt': 26, 'no': 9,
- '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}
-
- if language is None:
- self.config['language'] = 'en'
- else:
- if language not in self.config['valid_languages']:
- raise ValueError("Invalid language %s, options are: %s" % (
- language, self.config['valid_languages']
- ))
- else:
- self.config['language'] = language
-
- # The following url_ configs are based of the
- # http://thetvdb.com/wiki/index.php/Programmers_API
- self.config['base_url'] = "http://thetvdb.com"
-
- if self.config['search_all_languages']:
- self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=all" % self.config
- else:
- self.config['url_getSeries'] = u"%(base_url)s/api/GetSeries.php?seriesname=%%s&language=%(language)s" % 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_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_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
-
- def _getTempDir(self):
- """Returns the [system temp dir]/tvdb_api-u501 (or
- tvdb_api-myuser)
- """
- if hasattr(os, 'getuid'):
- uid = "u%d" % (os.getuid())
- else:
- # For Windows
- try:
- uid = getpass.getuser()
- except ImportError:
- return os.path.join(tempfile.gettempdir(), "tvdb_api")
-
- return os.path.join(tempfile.gettempdir(), "tvdb_api-%s" % (uid))
-
- def _loadUrl(self, url, recache = False, language=None):
- global lastTimeout
- try:
- log().debug("Retrieving URL %s" % url)
- resp = self.urlopener.open(url)
- if 'x-local-cache' in resp.headers:
- log().debug("URL %s was cached in %s" % (
- url,
- resp.headers['x-local-cache'])
- )
- if recache:
- log().debug("Attempting to recache %s" % url)
- resp.recache()
- except (IOError, urllib2.URLError), errormsg:
- if not str(errormsg).startswith('HTTP Error'):
- lastTimeout = datetime.datetime.now()
- raise tvdb_error("Could not connect to server: %s" % (errormsg))
-
-
- # handle gzipped content,
- # http://dbr.lighthouseapp.com/projects/13342/tickets/72-gzipped-data-patch
- if 'gzip' in resp.headers.get("Content-Encoding", ''):
- if gzip:
- stream = StringIO.StringIO(resp.read())
- gz = gzip.GzipFile(fileobj=stream)
- return gz.read()
-
- raise tvdb_error("Received gzip data from thetvdb.com, but could not correctly handle it")
-
- if 'application/zip' in resp.headers.get("Content-Type", ''):
- try:
- # TODO: The zip contains actors.xml and banners.xml, which are currently ignored [GH-20]
- log().debug("We recived a zip file unpacking now ...")
- zipdata = StringIO.StringIO()
- zipdata.write(resp.read())
- myzipfile = zipfile.ZipFile(zipdata)
- return myzipfile.read('%s.xml' % language)
- except zipfile.BadZipfile:
- if 'x-local-cache' in resp.headers:
- resp.delete_cache()
- raise tvdb_error("Bad zip file received from thetvdb.com, could not read it")
-
- return resp.read()
-
- def _getetsrc(self, url, language=None):
- """Loads a URL using caching, returns an ElementTree of the source
- """
- src = self._loadUrl(url, language=language)
- try:
- # TVDB doesn't sanitize \r (CR) from user input in some fields,
- # remove it to avoid errors. Change from SickBeard, from will14m
- return ElementTree.fromstring(src.rstrip("\r"))
- except SyntaxError:
- src = self._loadUrl(url, recache=True, language=language)
- try:
- return ElementTree.fromstring(src.rstrip("\r"))
- except SyntaxError, exceptionmsg:
- errormsg = "There was an error with the XML retrieved from thetvdb.com:\n%s" % (
- exceptionmsg
- )
-
- if self.config['cache_enabled']:
- errormsg += "\nFirst try emptying the cache folder at..\n%s" % (
- self.config['cache_location']
- )
-
- errormsg += "\nIf this does not resolve the issue, please try again later. If the error persists, report a bug on"
- errormsg += "\nhttp://dbr.lighthouseapp.com/projects/13342-tvdb_api/overview\n"
- raise tvdb_error(errormsg)
-
- def _setItem(self, sid, seas, ep, attrib, value):
- """Creates a new episode, creating Show(), Season() and
- Episode()s as required. Called by _getShowData to populate show
-
- Since the nice-to-use tvdb[1][24]['name] interface
- makes it impossible to do tvdb[1][24]['name] = "name"
- and still be capable of checking if an episode exists
- so we can raise tvdb_shownotfound, we have a slightly
- less pretty method of setting items.. but since the API
- is supposed to be read-only, this is the best way to
- do it!
- The problem is that calling tvdb[1][24]['episodename'] = "name"
- calls __getitem__ on tvdb[1], there is no way to check if
- tvdb.__dict__ should have a key "1" before we auto-create it
- """
- if sid not in self.shows:
- self.shows[sid] = Show()
- if seas not in self.shows[sid]:
- self.shows[sid][seas] = Season(show = self.shows[sid])
- if ep not in self.shows[sid][seas]:
- self.shows[sid][seas][ep] = Episode(season = self.shows[sid][seas])
- self.shows[sid][seas][ep][attrib] = value
-
- def _setShowData(self, sid, key, value):
- """Sets self.shows[sid] to a new Show instance, or sets the data
- """
- if sid not in self.shows:
- self.shows[sid] = Show()
- self.shows[sid].data[key] = value
-
- def _cleanData(self, data):
- """Cleans up strings returned by TheTVDB.com
-
- Issues corrected:
- - Replaces & with &
- - Trailing whitespace
- """
- data = data.replace(u"&", u"&")
- data = data.strip()
- return data
-
- def search(self, series):
- """This searches TheTVDB.com for the series name
- and returns the result list
- """
- series = urllib.quote(series.encode("utf-8"))
- log().debug("Searching for show %s" % series)
- seriesEt = self._getetsrc(self.config['url_getSeries'] % (series))
- allSeries = []
- for series in seriesEt:
- result = dict((k.tag.lower(), k.text) for k in series.getchildren())
- result['id'] = int(result['id'])
- result['lid'] = self.config['langabbv_to_id'][result['language']]
- log().debug('Found series %(seriesname)s' % result)
- allSeries.append(result)
-
- return allSeries
-
- def _getSeries(self, series):
- """This searches TheTVDB.com for the series name,
- 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
- BaseUI is used to select the first result.
- """
- allSeries = self.search(series)
-
- if len(allSeries) == 0:
- log().debug('Series result returned zero')
- raise tvdb_shownotfound("Show-name search returned zero results (cannot find show on TVDB)")
-
- if self.config['custom_ui'] is not None:
- log().debug("Using custom UI %s" % (repr(self.config['custom_ui'])))
- ui = self.config['custom_ui'](config = self.config)
- else:
- if not self.config['interactive']:
- log().debug('Auto-selecting first search result using BaseUI')
- ui = BaseUI(config = self.config)
- else:
- log().debug('Interactively selecting show using ConsoleUI')
- ui = ConsoleUI(config = self.config)
-
- return ui.selectSeries(allSeries)
-
- def _parseBanners(self, sid):
- """Parses banners XML, from
- http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/banners.xml
-
- Banners are retrieved using t['show name]['_banners'], for example:
-
- >>> t = Tvdb(banners = True)
- >>> t['scrubs']['_banners'].keys()
- ['fanart', 'poster', 'series', 'season']
- >>> t['scrubs']['_banners']['poster']['680x1000']['35308']['_bannerpath']
- u'http://thetvdb.com/banners/posters/76156-2.jpg'
- >>>
-
- Any key starting with an underscore has been processed (not the raw
- data from the XML)
-
- This interface will be improved in future versions.
- """
- log().debug('Getting season banners for %s' % (sid))
- bannersEt = self._getetsrc( self.config['url_seriesBanner'] % (sid) )
- banners = {}
- for cur_banner in bannersEt.findall('Banner'):
- bid = cur_banner.find('id').text
- btype = cur_banner.find('BannerType')
- btype2 = cur_banner.find('BannerType2')
- if btype is None or btype2 is None:
- continue
- btype, btype2 = btype.text, btype2.text
- if not btype in banners:
- banners[btype] = {}
- if not btype2 in banners[btype]:
- banners[btype][btype2] = {}
- if not bid in banners[btype][btype2]:
- banners[btype][btype2][bid] = {}
-
- for cur_element in cur_banner.getchildren():
- tag = cur_element.tag.lower()
- value = cur_element.text
- if tag is None or value is None:
- continue
- tag, value = tag.lower(), value.lower()
- banners[btype][btype2][bid][tag] = value
-
- for k, v in banners[btype][btype2][bid].items():
- if k.endswith("path"):
- new_key = "_%s" % (k)
- log().debug("Transforming %s to %s" % (k, new_key))
- new_url = self.config['url_artworkPrefix'] % (v)
- banners[btype][btype2][bid][new_key] = new_url
-
- self._setShowData(sid, "_banners", banners)
-
- def _parseActors(self, sid):
- """Parsers actors XML, from
- http://thetvdb.com/api/[APIKEY]/series/[SERIES ID]/actors.xml
-
- Actors are retrieved using t['show name]['_actors'], for example:
-
- >>> t = Tvdb(actors = True)
- >>> actors = t['scrubs']['_actors']
- >>> type(actors)
-
- >>> type(actors[0])
-
- >>> actors[0]
-
- >>> sorted(actors[0].keys())
- ['id', 'image', 'name', 'role', 'sortorder']
- >>> actors[0]['name']
- u'Zach Braff'
- >>> actors[0]['image']
- u'http://thetvdb.com/banners/actors/43640.jpg'
-
- Any key starting with an underscore has been processed (not the raw
- data from the XML)
- """
- log().debug("Getting actors for %s" % (sid))
- actorsEt = self._getetsrc(self.config['url_actorsInfo'] % (sid))
-
- cur_actors = Actors()
- for curActorItem in actorsEt.findall("Actor"):
- curActor = Actor()
- for curInfo in curActorItem:
- tag = curInfo.tag.lower()
- value = curInfo.text
- if value is not None:
- if tag == "image":
- value = self.config['url_artworkPrefix'] % (value)
- else:
- value = self._cleanData(value)
- curActor[tag] = value
- cur_actors.append(curActor)
- self._setShowData(sid, '_actors', cur_actors)
-
- def _getShowData(self, sid, language):
- """Takes a series ID, gets the epInfo URL and parses the TVDB
- XML file into the shows dict in layout:
- shows[series_id][season_number][episode_number]
- """
-
- if self.config['language'] is None:
- log().debug('Config language is none, using show language')
- if language is None:
- raise tvdb_error("config['language'] was None, this should not happen")
- getShowInLanguage = language
- else:
- log().debug(
- 'Configured language %s override show language of %s' % (
- self.config['language'],
- language
- )
- )
- getShowInLanguage = self.config['language']
-
- # Parse show information
- log().debug('Getting all series data for %s' % (sid))
- seriesInfoEt = self._getetsrc(
- self.config['url_seriesInfo'] % (sid, getShowInLanguage)
- )
- for curInfo in seriesInfoEt.findall("Series")[0]:
- tag = curInfo.tag.lower()
- value = curInfo.text
-
- if value is not None:
- if tag in ['banner', 'fanart', 'poster']:
- value = self.config['url_artworkPrefix'] % (value)
- else:
- value = self._cleanData(value)
-
- self._setShowData(sid, tag, value)
-
- # Parse banners
- if self.config['banners_enabled']:
- self._parseBanners(sid)
-
- # Parse actors
- if self.config['actors_enabled']:
- self._parseActors(sid)
-
- # Parse episode data
- log().debug('Getting all episodes of %s' % (sid))
-
- if self.config['useZip']:
- url = self.config['url_epInfo_zip'] % (sid, language)
- else:
- url = self.config['url_epInfo'] % (sid, language)
-
- epsEt = self._getetsrc( url, language=language)
-
- for cur_ep in epsEt.findall("Episode"):
- seas_no = int(cur_ep.find('SeasonNumber').text)
- ep_no = int(cur_ep.find('EpisodeNumber').text)
- for cur_item in cur_ep.getchildren():
- tag = cur_item.tag.lower()
- value = cur_item.text
- if value is not None:
- if tag == 'filename':
- value = self.config['url_artworkPrefix'] % (value)
- else:
- value = self._cleanData(value)
- self._setItem(sid, seas_no, ep_no, tag, value)
-
- def _nameToSid(self, name):
- """Takes show name, returns the correct series ID (if the show has
- already been grabbed), or grabs all episodes and returns
- the correct SID.
- """
- if name in self.corrections:
- log().debug('Correcting %s to %s' % (name, self.corrections[name]) )
- sid = self.corrections[name]
- else:
- log().debug('Getting show %s' % (name))
- selected_series = self._getSeries( name )
- sname, sid = selected_series['seriesname'], selected_series['id']
- log().debug('Got %(seriesname)s, id %(id)s' % selected_series)
-
- self.corrections[name] = sid
- self._getShowData(selected_series['id'], selected_series['language'])
-
- return sid
-
- def __getitem__(self, key):
- """Handles tvdb_instance['seriesname'] calls.
- The dict index should be the show id
- """
- if isinstance(key, (int, long)):
- # Item is integer, treat as show id
- if key not in self.shows:
- self._getShowData(key, self.config['language'])
- return self.shows[key]
-
- key = key.lower() # make key lower case
- sid = self._nameToSid(key)
- log().debug('Got series id %s' % (sid))
- return self.shows[sid]
-
- def __repr__(self):
- return str(self.shows)
-
-
-def main():
- """Simple example of using tvdb_api - it just
- grabs an episode name interactively.
- """
- import logging
- logging.basicConfig(level=logging.DEBUG)
-
- tvdb_instance = Tvdb(interactive=True, cache=False)
- print tvdb_instance['Lost']['seriesname']
- print tvdb_instance['Lost'][1][4]['episodename']
-
-if __name__ == '__main__':
- main()
diff --git a/libs/thetvdb/tvdb_cache.py b/libs/thetvdb/tvdb_cache.py
deleted file mode 100644
index d77c5457..00000000
--- a/libs/thetvdb/tvdb_cache.py
+++ /dev/null
@@ -1,251 +0,0 @@
-#!/usr/bin/env python
-#encoding:utf-8
-#author:dbr/Ben
-#project:tvdb_api
-#repository:http://github.com/dbr/tvdb_api
-#license:unlicense (http://unlicense.org/)
-
-"""
-urllib2 caching handler
-Modified from http://code.activestate.com/recipes/491261/
-"""
-from __future__ import with_statement
-
-__author__ = "dbr/Ben"
-__version__ = "1.8.2"
-
-import os
-import time
-import errno
-import httplib
-import urllib2
-import StringIO
-from hashlib import md5
-from threading import RLock
-
-cache_lock = RLock()
-
-def locked_function(origfunc):
- """Decorator to execute function under lock"""
- def wrapped(*args, **kwargs):
- cache_lock.acquire()
- try:
- return origfunc(*args, **kwargs)
- finally:
- cache_lock.release()
- return wrapped
-
-def calculate_cache_path(cache_location, url):
- """Checks if [cache_location]/[hash_of_url].headers and .body exist
- """
- thumb = md5(url).hexdigest()
- header = os.path.join(cache_location, thumb + ".headers")
- body = os.path.join(cache_location, thumb + ".body")
- return header, body
-
-def check_cache_time(path, max_age):
- """Checks if a file has been created/modified in the [last max_age] seconds.
- False means the file is too old (or doesn't exist), True means it is
- up-to-date and valid"""
- if not os.path.isfile(path):
- return False
- cache_modified_time = os.stat(path).st_mtime
- time_now = time.time()
- if cache_modified_time < time_now - max_age:
- # Cache is old
- return False
- else:
- return True
-
-@locked_function
-def exists_in_cache(cache_location, url, max_age):
- """Returns if header AND body cache file exist (and are up-to-date)"""
- hpath, bpath = calculate_cache_path(cache_location, url)
- if os.path.exists(hpath) and os.path.exists(bpath):
- return(
- check_cache_time(hpath, max_age)
- and check_cache_time(bpath, max_age)
- )
- else:
- # File does not exist
- return False
-
-@locked_function
-def store_in_cache(cache_location, url, response):
- """Tries to store response in cache."""
- hpath, bpath = calculate_cache_path(cache_location, url)
- try:
- outf = open(hpath, "wb")
- headers = str(response.info())
- outf.write(headers)
- outf.close()
-
- outf = open(bpath, "wb")
- outf.write(response.read())
- outf.close()
- except IOError:
- return True
- else:
- return False
-
-@locked_function
-def delete_from_cache(cache_location, url):
- """Deletes a response in cache."""
- hpath, bpath = calculate_cache_path(cache_location, url)
- try:
- if os.path.exists(hpath):
- os.remove(hpath)
- if os.path.exists(bpath):
- os.remove(bpath)
- except IOError:
- return True
- else:
- return False
-
-class CacheHandler(urllib2.BaseHandler):
- """Stores responses in a persistant on-disk cache.
-
- If a subsequent GET request is made for the same URL, the stored
- response is returned, saving time, resources and bandwidth
- """
- @locked_function
- def __init__(self, cache_location, max_age = 21600):
- """The location of the cache directory"""
- self.max_age = max_age
- self.cache_location = cache_location
- if not os.path.exists(self.cache_location):
- try:
- os.mkdir(self.cache_location)
- except OSError, e:
- if e.errno == errno.EEXIST and os.path.isdir(self.cache_location):
- # File exists, and it's a directory,
- # another process beat us to creating this dir, that's OK.
- pass
- else:
- # Our target dir is already a file, or different error,
- # relay the error!
- raise
-
- def default_open(self, request):
- """Handles GET requests, if the response is cached it returns it
- """
- if request.get_method() is not "GET":
- return None # let the next handler try to handle the request
-
- if exists_in_cache(
- self.cache_location, request.get_full_url(), self.max_age
- ):
- return CachedResponse(
- self.cache_location,
- request.get_full_url(),
- set_cache_header = True
- )
- else:
- return None
-
- def http_response(self, request, response):
- """Gets a HTTP response, if it was a GET request and the status code
- starts with 2 (200 OK etc) it caches it and returns a CachedResponse
- """
- if (request.get_method() == "GET"
- and str(response.code).startswith("2")
- ):
- if 'x-local-cache' not in response.info():
- # Response is not cached
- set_cache_header = store_in_cache(
- self.cache_location,
- request.get_full_url(),
- response
- )
- else:
- set_cache_header = True
-
- return CachedResponse(
- self.cache_location,
- request.get_full_url(),
- set_cache_header = set_cache_header
- )
- else:
- return response
-
-class CachedResponse(StringIO.StringIO):
- """An urllib2.response-like object for cached responses.
-
- To determine if a response is cached or coming directly from
- the network, check the x-local-cache header rather than the object type.
- """
-
- @locked_function
- def __init__(self, cache_location, url, set_cache_header=True):
- self.cache_location = cache_location
- hpath, bpath = calculate_cache_path(cache_location, url)
-
- StringIO.StringIO.__init__(self, file(bpath, "rb").read())
-
- self.url = url
- self.code = 200
- self.msg = "OK"
- headerbuf = file(hpath, "rb").read()
- if set_cache_header:
- headerbuf += "x-local-cache: %s\r\n" % (bpath)
- self.headers = httplib.HTTPMessage(StringIO.StringIO(headerbuf))
-
- def info(self):
- """Returns headers
- """
- return self.headers
-
- def geturl(self):
- """Returns original URL
- """
- return self.url
-
- @locked_function
- def recache(self):
- new_request = urllib2.urlopen(self.url)
- set_cache_header = store_in_cache(
- self.cache_location,
- new_request.url,
- new_request
- )
- CachedResponse.__init__(self, self.cache_location, self.url, True)
-
- @locked_function
- def delete_cache(self):
- delete_from_cache(
- self.cache_location,
- self.url
- )
-
-
-if __name__ == "__main__":
- def main():
- """Quick test/example of CacheHandler"""
- opener = urllib2.build_opener(CacheHandler("/tmp/"))
- response = opener.open("http://google.com")
- print response.headers
- print "Response:", response.read()
-
- response.recache()
- print response.headers
- print "After recache:", response.read()
-
- # Test usage in threads
- from threading import Thread
- class CacheThreadTest(Thread):
- lastdata = None
- def run(self):
- req = opener.open("http://google.com")
- newdata = req.read()
- if self.lastdata is None:
- self.lastdata = newdata
- assert self.lastdata == newdata, "Data was not consistent, uhoh"
- req.recache()
- threads = [CacheThreadTest() for x in range(50)]
- print "Starting threads"
- [t.start() for t in threads]
- print "..done"
- print "Joining threads"
- [t.join() for t in threads]
- print "..done"
- main()
diff --git a/libs/thetvdb/tvdb_exceptions.py b/libs/thetvdb/tvdb_exceptions.py
deleted file mode 100644
index cacbb936..00000000
--- a/libs/thetvdb/tvdb_exceptions.py
+++ /dev/null
@@ -1,52 +0,0 @@
-#!/usr/bin/env python
-#encoding:utf-8
-#author:dbr/Ben
-#project:tvdb_api
-#repository:http://github.com/dbr/tvdb_api
-#license:unlicense (http://unlicense.org/)
-
-"""Custom exceptions used or raised by tvdb_api
-"""
-
-__author__ = "dbr/Ben"
-__version__ = "1.8.2"
-
-__all__ = ["tvdb_error", "tvdb_userabort", "tvdb_shownotfound",
-"tvdb_seasonnotfound", "tvdb_episodenotfound", "tvdb_attributenotfound"]
-
-class tvdb_exception(Exception):
- """Any exception generated by tvdb_api
- """
- pass
-
-class tvdb_error(tvdb_exception):
- """An error with thetvdb.com (Cannot connect, for example)
- """
- pass
-
-class tvdb_userabort(tvdb_exception):
- """User aborted the interactive selection (via
- the q command, ^c etc)
- """
- pass
-
-class tvdb_shownotfound(tvdb_exception):
- """Show cannot be found on thetvdb.com (non-existant show)
- """
- pass
-
-class tvdb_seasonnotfound(tvdb_exception):
- """Season cannot be found on thetvdb.com
- """
- pass
-
-class tvdb_episodenotfound(tvdb_exception):
- """Episode cannot be found on thetvdb.com
- """
- pass
-
-class tvdb_attributenotfound(tvdb_exception):
- """Raised if an episode does not have the requested
- attribute (such as a episode name)
- """
- pass
diff --git a/libs/thetvdb/tvdb_ui.py b/libs/thetvdb/tvdb_ui.py
deleted file mode 100644
index a4b6e95d..00000000
--- a/libs/thetvdb/tvdb_ui.py
+++ /dev/null
@@ -1,153 +0,0 @@
-#!/usr/bin/env python
-#encoding:utf-8
-#author:dbr/Ben
-#project:tvdb_api
-#repository:http://github.com/dbr/tvdb_api
-#license:unlicense (http://unlicense.org/)
-
-"""Contains included user interfaces for Tvdb show selection.
-
-A UI is a callback. A class, it's __init__ function takes two arguments:
-
-- config, which is the Tvdb config dict, setup in tvdb_api.py
-- log, which is Tvdb's logger instance (which uses the logging module). You can
-call log.info() log.warning() etc
-
-It must have a method "selectSeries", this is passed a list of dicts, each dict
-contains the the keys "name" (human readable show name), and "sid" (the shows
-ID as on thetvdb.com). For example:
-
-[{'name': u'Lost', 'sid': u'73739'},
- {'name': u'Lost Universe', 'sid': u'73181'}]
-
-The "selectSeries" method must return the appropriate dict, or it can raise
-tvdb_userabort (if the selection is aborted), tvdb_shownotfound (if the show
-cannot be found).
-
-A simple example callback, which returns a random series:
-
->>> import random
->>> from tvdb_ui import BaseUI
->>> class RandomUI(BaseUI):
-... def selectSeries(self, allSeries):
-... import random
-... return random.choice(allSeries)
-
-Then to use it..
-
->>> from tvdb_api import Tvdb
->>> t = Tvdb(custom_ui = RandomUI)
->>> random_matching_series = t['Lost']
->>> type(random_matching_series)
-
-"""
-
-__author__ = "dbr/Ben"
-__version__ = "1.8.2"
-
-import logging
-import warnings
-
-from tvdb_exceptions import tvdb_userabort
-
-def log():
- return logging.getLogger(__name__)
-
-class BaseUI:
- """Default non-interactive UI, which auto-selects first results
- """
- def __init__(self, config, log = None):
- self.config = config
- if log is not None:
- warnings.warn("the UI's log parameter is deprecated, instead use\n"
- "use import logging; logging.getLogger('ui').info('blah')\n"
- "The self.log attribute will be removed in the next version")
- self.log = logging.getLogger(__name__)
-
- def selectSeries(self, allSeries):
- return allSeries[0]
-
-
-class ConsoleUI(BaseUI):
- """Interactively allows the user to select a show from a console based UI
- """
-
- def _displaySeries(self, allSeries, limit = 6):
- """Helper function, lists series with corresponding ID
- """
- if limit is not None:
- toshow = allSeries[:limit]
- else:
- toshow = allSeries
-
- print "TVDB Search Results:"
- for i, cshow in enumerate(toshow):
- i_show = i + 1 # Start at more human readable number 1 (not 0)
- log().debug('Showing allSeries[%s], series %s)' % (i_show, allSeries[i]['seriesname']))
- if i == 0:
- extra = " (default)"
- else:
- extra = ""
-
- print "%s -> %s [%s] # http://thetvdb.com/?tab=series&id=%s&lid=%s%s" % (
- i_show,
- cshow['seriesname'].encode("UTF-8", "ignore"),
- cshow['language'].encode("UTF-8", "ignore"),
- str(cshow['id']),
- cshow['lid'],
- extra
- )
-
- def selectSeries(self, allSeries):
- self._displaySeries(allSeries)
-
- if len(allSeries) == 1:
- # Single result, return it!
- print "Automatically selecting only result"
- return allSeries[0]
-
- if self.config['select_first'] is True:
- print "Automatically returning first search result"
- return allSeries[0]
-
- while True: # return breaks this loop
- try:
- print "Enter choice (first number, return for default, 'all', ? for help):"
- ans = raw_input()
- except KeyboardInterrupt:
- raise tvdb_userabort("User aborted (^c keyboard interupt)")
- except EOFError:
- raise tvdb_userabort("User aborted (EOF received)")
-
- log().debug('Got choice of: %s' % (ans))
- try:
- selected_id = int(ans) - 1 # The human entered 1 as first result, not zero
- except ValueError: # Input was not number
- if len(ans.strip()) == 0:
- # Default option
- log().debug('Default option, returning first series')
- return allSeries[0]
- if ans == "q":
- log().debug('Got quit command (q)')
- raise tvdb_userabort("User aborted ('q' quit command)")
- elif ans == "?":
- print "## Help"
- print "# Enter the number that corresponds to the correct show."
- print "# a - display all results"
- print "# all - display all results"
- print "# ? - this help"
- print "# q - abort tvnamer"
- print "# Press return with no input to select first result"
- elif ans.lower() in ["a", "all"]:
- self._displaySeries(allSeries, limit = None)
- else:
- log().debug('Unknown keypress %s' % (ans))
- else:
- log().debug('Trying to return ID: %d' % (selected_id))
- try:
- return allSeries[selected_id]
- except IndexError:
- log().debug('Invalid show number entered!')
- print "Invalid number (%s) selected!"
- self._displaySeries(allSeries)
-
diff --git a/libs/tvdb_api b/libs/tvdb_api
new file mode 160000
index 00000000..8441558b
--- /dev/null
+++ b/libs/tvdb_api
@@ -0,0 +1 @@
+Subproject commit 8441558b6758f6d79fff0691f6b2a77d3d06fd99