From 15a19949b89032b3078489722d3dda23a17e991c Mon Sep 17 00:00:00 2001 From: Sami Haahtinen Date: Sun, 26 Jan 2014 19:26:15 +0200 Subject: [PATCH 01/16] Fix rTorrent connectivity The combination of cleanHost and rTorrent.connect issues caused rTorrent connections to fail. This update fixes cleanHost() so that it can actually cope with SSL based hosts and finishes the migration to cleanHost() in connect() Conflicts: couchpotato/core/helpers/variable.py --- couchpotato/core/downloaders/rtorrent/main.py | 2 +- couchpotato/core/helpers/variable.py | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 27177756..cfd1dce0 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -42,7 +42,7 @@ class rTorrent(Downloader): if self.rt is not None: return self.rt - url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + '/' + self.conf('rpc_url').strip('/ ') + '/' + url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + self.conf('rpc_url') if self.conf('username') and self.conf('password'): self.rt = RTorrent( diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 47810a66..a586ceff 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -136,9 +136,25 @@ def getExt(filename): def cleanHost(host, protocol = True, ssl = False, username = None, password = None): + """Return a cleaned up host with given url options set + + Changes protocol to https if ssl is set to True and http if ssl is set to false. + >>> cleanHost("localhost:80", ssl=True) + 'https://localhost:80/' + >>> cleanHost("localhost:80", ssl=False) + 'http://localhost:80/' + + Username and password is managed with the username and password variables + >>> cleanHost("localhost:80", username="user", password="passwd") + 'http://user:passwd@localhost:80/' + + Output without scheme (protocol) can be forced with protocol=False + >>> cleanHost("localhost:80", protocol=False) + 'localhost:80' + """ if not '://' in host and protocol: - host = 'https://' if ssl else 'http://' + host + host = ('https://' if ssl else 'http://') + host if not protocol: host = host.split('://', 1)[-1] From 9d55ecffe9911d8d5f49812a0f304c80934d17a7 Mon Sep 17 00:00:00 2001 From: Ruud Date: Mon, 27 Jan 2014 21:58:48 +0100 Subject: [PATCH 02/16] Add log var --- couchpotato/core/media/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/media/__init__.py b/couchpotato/core/media/__init__.py index 512c52c2..cf9302b1 100644 --- a/couchpotato/core/media/__init__.py +++ b/couchpotato/core/media/__init__.py @@ -1,9 +1,11 @@ import traceback -from couchpotato import get_session +from couchpotato import get_session, CPLog from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.plugins.base import Plugin from couchpotato.core.settings.model import Media +log = CPLog(__name__) + class MediaBase(Plugin): From 461e469f2853f5048d4743151209439ecbd746d9 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Thu, 6 Feb 2014 22:18:41 +1300 Subject: [PATCH 03/16] Updated rtorrent-python library - Fixed bencode encoding bug with long types --- libs/rtorrent/lib/bencode.py | 2 +- libs/rtorrent/lib/torrentparser.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/rtorrent/lib/bencode.py b/libs/rtorrent/lib/bencode.py index 97bd2f0e..ba99ef93 100755 --- a/libs/rtorrent/lib/bencode.py +++ b/libs/rtorrent/lib/bencode.py @@ -267,7 +267,7 @@ def _encode_dict(data): def encode(data): if isinstance(data, bool): return False - elif isinstance(data, int): + elif isinstance(data, (int, long)): return _encode_int(data) elif isinstance(data, bytes): return _encode_string(data) diff --git a/libs/rtorrent/lib/torrentparser.py b/libs/rtorrent/lib/torrentparser.py index 30170d32..b339bce4 100755 --- a/libs/rtorrent/lib/torrentparser.py +++ b/libs/rtorrent/lib/torrentparser.py @@ -90,10 +90,10 @@ class TorrentParser(): def _calc_info_hash(self): self.info_hash = None if "info" in self._torrent_decoded.keys(): - info_encoded = bencode.encode(self._torrent_decoded["info"]) + info_encoded = bencode.encode(self._torrent_decoded["info"]) - if info_encoded: - self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper() + if info_encoded: + self.info_hash = hashlib.sha1(info_encoded).hexdigest().upper() return(self.info_hash) From 62524e01e158caac6a94eb5b6bf84fa0774151e2 Mon Sep 17 00:00:00 2001 From: Ramon van Dam Date: Fri, 7 Feb 2014 12:08:17 +0100 Subject: [PATCH 04/16] * Added category 'bd50' (BR-Disk) to Torrent provider TorrentBytes * Changed category identifier for category 'brrip' for Torrent provider TorrentBytes (see issue #2795) --- couchpotato/core/providers/torrent/torrentbytes/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/providers/torrent/torrentbytes/main.py b/couchpotato/core/providers/torrent/torrentbytes/main.py index 91939e78..603da6e0 100644 --- a/couchpotato/core/providers/torrent/torrentbytes/main.py +++ b/couchpotato/core/providers/torrent/torrentbytes/main.py @@ -20,12 +20,12 @@ class TorrentBytes(TorrentProvider): } cat_ids = [ - ([5], ['720p', '1080p']), + ([5], ['720p', '1080p', 'bd50']), ([19], ['cam']), ([19], ['ts', 'tc']), ([19], ['r5', 'scr']), ([19], ['dvdrip']), - ([5], ['brrip']), + ([19], ['brrip']), ([20], ['dvdr']), ] From a2c87e1b7d4af76ac33ebde1424c7ecbe8584f35 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 8 Feb 2014 03:22:59 +1300 Subject: [PATCH 05/16] Fixed bug where changes to rtorrent settings wouldn't take effect until a restart --- couchpotato/core/downloaders/rtorrent/main.py | 9 +++++++++ couchpotato/core/settings/__init__.py | 1 + 2 files changed, 10 insertions(+) diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index cfd1dce0..4fdecc89 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -24,6 +24,7 @@ class rTorrent(Downloader): super(rTorrent, self).__init__() addEvent('app.load', self.migrate) + addEvent('setting.save.rtorrent.*.after', self.settingsChanged) def migrate(self): @@ -37,6 +38,14 @@ class rTorrent(Downloader): self.deleteConf('url') + def settingsChanged(self): + # Reset active connection if settings have changed + if self.rt: + log.debug('Settings have changed, closing active connection') + + self.rt = None + return True + def connect(self): # Already connected? if self.rt is not None: diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py index 3b575176..0e65c778 100644 --- a/couchpotato/core/settings/__init__.py +++ b/couchpotato/core/settings/__init__.py @@ -200,6 +200,7 @@ class Settings(object): # After save (for re-interval etc) fireEvent('setting.save.%s.%s.after' % (section, option), single = True) + fireEvent('setting.save.%s.*.after' % section, single = True) return { 'success': True, From 3380e20e3aad6903172c07e989f64252f6160593 Mon Sep 17 00:00:00 2001 From: Dean Gardiner Date: Sat, 8 Feb 2014 03:25:11 +1300 Subject: [PATCH 06/16] Cleaned up naming of functions in rtorrent downloader --- couchpotato/core/downloaders/rtorrent/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py index 4fdecc89..1fbae354 100755 --- a/couchpotato/core/downloaders/rtorrent/main.py +++ b/couchpotato/core/downloaders/rtorrent/main.py @@ -64,7 +64,7 @@ class rTorrent(Downloader): return self.rt - def _update_provider_group(self, name, data): + def updateProviderGroup(self, name, data): if data.get('seed_time'): log.info('seeding time ignored, not supported') @@ -113,7 +113,7 @@ class rTorrent(Downloader): return False group_name = 'cp_' + data.get('provider').lower() - if not self._update_provider_group(group_name, data): + if not self.updateProviderGroup(group_name, data): return False torrent_params = {} From 8dfb0d1d5c498c7a882a7f5454513a7a4650a033 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sat, 15 Feb 2014 19:47:55 +0100 Subject: [PATCH 07/16] Fire events after tab add --- couchpotato/static/scripts/page/settings.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index 9c9453e0..74f3c7f0 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -196,13 +196,13 @@ Page.Settings = new Class({ }); setTimeout(function(){ - self.fireEvent('create'); - self.openTab(); - self.el.adopt( self.tabs_container, self.containers ); + + self.fireEvent('create'); + self.openTab(); }, 0); }, From 886a271d1926d33085f95e9a3d7c580186e2016d Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 09:42:47 +0100 Subject: [PATCH 08/16] Use correct ordering for request arrays. fix #2810 --- couchpotato/core/helpers/request.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/helpers/request.py b/couchpotato/core/helpers/request.py index 8a10f078..b62ab689 100644 --- a/couchpotato/core/helpers/request.py +++ b/couchpotato/core/helpers/request.py @@ -44,7 +44,10 @@ def dictToList(params): new = {} for x, value in params.items(): try: - new_value = [dictToList(value[k]) for k in sorted(value.keys(), cmp = natcmp)] + convert = lambda text: int(text) if text.isdigit() else text.lower() + alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)] + sorted_keys = sorted(value.keys(), key = alphanum_key) + new_value = [dictToList(value[k]) for k in sorted_keys] except: new_value = value From 4831c805986570d77613ea265dacd6dadd008e83 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 09:59:37 +0100 Subject: [PATCH 09/16] Update nzbclub url --- couchpotato/core/providers/nzb/nzbclub/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py index 778cdbcd..5c151dc4 100644 --- a/couchpotato/core/providers/nzb/nzbclub/main.py +++ b/couchpotato/core/providers/nzb/nzbclub/main.py @@ -13,7 +13,7 @@ log = CPLog(__name__) class NZBClub(NZBProvider, RSS): urls = { - 'search': 'http://www.nzbclub.com/nzbfeed.aspx?%s', + 'search': 'http://www.nzbclub.com/nzbfeeds.aspx?%s', } http_time_between_calls = 4 #seconds From 888ee07f65b76645129ddf3a96bd2d37211df983 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 10:27:31 +0100 Subject: [PATCH 10/16] Check responsecodes --- couchpotato/core/plugins/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 0625535e..717a875f 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -172,7 +172,10 @@ class Plugin(object): log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data')) response = r.request(method, url, verify = False, **kwargs) - data = response.content + if response.status_code != requests.codes.ok: + data = response.content + else: + response.raise_for_status() self.http_failed_request[host] = 0 except (IOError, MaxRetryError, Timeout): From f22778aacbaddbfc144fb31d66ad925c6db60ffa Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 10:40:54 +0100 Subject: [PATCH 11/16] Use proper check --- couchpotato/core/plugins/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index 717a875f..e70c3bc6 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -172,7 +172,7 @@ class Plugin(object): log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data')) response = r.request(method, url, verify = False, **kwargs) - if response.status_code != requests.codes.ok: + if response.status_code == requests.codes.ok: data = response.content else: response.raise_for_status() From a1c0b000a45baf82782cefed624cc5f5f79d4895 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 10:48:48 +0100 Subject: [PATCH 12/16] Update TMDB api --- libs/tmdb3/__init__.py | 3 +- libs/tmdb3/cache.py | 43 ++- libs/tmdb3/cache_engine.py | 30 +- libs/tmdb3/cache_file.py | 55 +-- libs/tmdb3/cache_null.py | 18 +- libs/tmdb3/locales.py | 24 +- libs/tmdb3/pager.py | 31 +- libs/tmdb3/request.py | 66 ++-- libs/tmdb3/tmdb_api.py | 651 +++++++++++++++++++++++----------- libs/tmdb3/tmdb_auth.py | 35 +- libs/tmdb3/tmdb_exceptions.py | 90 +++-- libs/tmdb3/util.py | 201 ++++++----- 12 files changed, 797 insertions(+), 450 deletions(-) diff --git a/libs/tmdb3/__init__.py b/libs/tmdb3/__init__.py index 92ca5510..d5e35b30 100755 --- a/libs/tmdb3/__init__.py +++ b/libs/tmdb3/__init__.py @@ -2,7 +2,8 @@ from tmdb_api import Configuration, searchMovie, searchMovieWithYear, \ searchPerson, searchStudio, searchList, searchCollection, \ - Person, Movie, Collection, Genre, List, __version__ + searchSeries, Person, Movie, Collection, Genre, List, \ + Series, Studio, Network, Episode, Season, __version__ from request import set_key, set_cache from locales import get_locale, set_locale from tmdb_auth import get_session, set_session diff --git a/libs/tmdb3/cache.py b/libs/tmdb3/cache.py index 3b106774..463d7a2c 100755 --- a/libs/tmdb3/cache.py +++ b/libs/tmdb3/cache.py @@ -7,20 +7,27 @@ # Purpose: Caching framework to store TMDb API results #----------------------- +import time +import os + from tmdb_exceptions import * from cache_engine import Engines import cache_null import cache_file -class Cache( object ): + +class Cache(object): """ - This class implements a persistent cache, backed in a file specified in - the object creation. The file is protected for safe, concurrent access - by multiple instances using flock. - This cache uses JSON for speed and storage efficiency, so only simple - data types are supported. - Data is stored in a simple format {key:(expiretimestamp, data)} + This class implements a cache framework, allowing selecting of a + pluggable engine. The framework stores data in a key/value manner, + along with a lifetime, after which data will be expired and + pulled fresh next time it is requested from the cache. + + This class defines a wrapper to be used with query functions. The + wrapper will automatically cache the inputs and outputs of the + wrapped function, pulling the output from local storage for + subsequent calls with those inputs. """ def __init__(self, engine=None, *args, **kwargs): self._engine = None @@ -37,7 +44,7 @@ class Cache( object ): self._age = max(self._age, obj.creation) def _expire(self): - for k,v in self._data.items(): + for k, v in self._data.items(): if v.expired: del self._data[k] @@ -87,19 +94,22 @@ class Cache( object ): self.__doc__ = func.__doc__ def __call__(self, *args, **kwargs): - if self.func is None: # decorator is waiting to be given a function + if self.func is None: + # decorator is waiting to be given a function if len(kwargs) or (len(args) != 1): - raise TMDBCacheError('Cache.Cached decorator must be called '+\ - 'a single callable argument before it '+\ - 'be used.') + raise TMDBCacheError( + 'Cache.Cached decorator must be called a single ' + + 'callable argument before it be used.') elif args[0] is None: - raise TMDBCacheError('Cache.Cached decorator called before '+\ - 'being given a function to wrap.') + raise TMDBCacheError( + 'Cache.Cached decorator called before being given ' + + 'a function to wrap.') elif not callable(args[0]): - raise TMDBCacheError('Cache.Cached must be provided a '+\ - 'callable object.') + raise TMDBCacheError( + 'Cache.Cached must be provided a callable object.') return self.__class__(self.cache, self.callback, args[0]) elif self.inst.lifetime == 0: + # lifetime of zero means never cache return self.func(*args, **kwargs) else: key = self.callback() @@ -118,4 +128,3 @@ class Cache( object ): func = self.func.__get__(inst, owner) callback = self.callback.__get__(inst, owner) return self.__class__(self.cache, callback, func, inst) - diff --git a/libs/tmdb3/cache_engine.py b/libs/tmdb3/cache_engine.py index 99ad4cda..11019551 100755 --- a/libs/tmdb3/cache_engine.py +++ b/libs/tmdb3/cache_engine.py @@ -10,35 +10,46 @@ import time from weakref import ref -class Engines( object ): + +class Engines(object): + """ + Static collector for engines to register against. + """ def __init__(self): self._engines = {} + def register(self, engine): self._engines[engine.__name__] = engine self._engines[engine.name] = engine + def __getitem__(self, key): return self._engines[key] + def __contains__(self, key): return self._engines.__contains__(key) + Engines = Engines() -class CacheEngineType( type ): + +class CacheEngineType(type): """ Cache Engine Metaclass that registers new engines against the cache for named selection and use. """ - def __init__(mcs, name, bases, attrs): - super(CacheEngineType, mcs).__init__(name, bases, attrs) + def __init__(cls, name, bases, attrs): + super(CacheEngineType, cls).__init__(name, bases, attrs) if name != 'CacheEngine': # skip base class - Engines.register(mcs) + Engines.register(cls) -class CacheEngine( object ): + +class CacheEngine(object): __metaclass__ = CacheEngineType - name = 'unspecified' + def __init__(self, parent): self.parent = ref(parent) + def configure(self): raise RuntimeError def get(self, date): @@ -48,7 +59,8 @@ class CacheEngine( object ): def expire(self, key): raise RuntimeError -class CacheObject( object ): + +class CacheObject(object): """ Cache object class, containing one stored record. """ @@ -64,7 +76,7 @@ class CacheObject( object ): @property def expired(self): - return (self.remaining == 0) + return self.remaining == 0 @property def remaining(self): diff --git a/libs/tmdb3/cache_file.py b/libs/tmdb3/cache_file.py index 5918071a..4e965815 100755 --- a/libs/tmdb3/cache_file.py +++ b/libs/tmdb3/cache_file.py @@ -12,6 +12,7 @@ import struct import errno import json +import time import os import io @@ -54,11 +55,11 @@ def _donothing(*args, **kwargs): try: import fcntl - class Flock( object ): + class Flock(object): """ - Context manager to flock file for the duration the object exists. - Referenced file will be automatically unflocked as the interpreter - exits the context. + Context manager to flock file for the duration the object + exists. Referenced file will be automatically unflocked as the + interpreter exits the context. Supports an optional callback to process the error and optionally suppress it. """ @@ -69,8 +70,10 @@ try: self.fileobj = fileobj self.operation = operation self.callback = callback + def __enter__(self): fcntl.flock(self.fileobj, self.operation) + def __exit__(self, exc_type, exc_value, exc_tb): suppress = False if callable(self.callback): @@ -101,9 +104,11 @@ except ImportError: self.fileobj = fileobj self.operation = operation self.callback = callback + def __enter__(self): self.size = os.path.getsize(self.fileobj.name) msvcrt.locking(self.fileobj.fileno(), self.operation, self.size) + def __exit__(self, exc_type, exc_value, exc_tb): suppress = False if callable(self.callback): @@ -118,7 +123,7 @@ except ImportError: if filename.startswith('~'): # check for home directory return os.path.expanduser(filename) - elif (ord(filename[0]) in (range(65,91)+range(99,123))) \ + elif (ord(filename[0]) in (range(65, 91) + range(99, 123))) \ and (filename[1:3] == ':\\'): # check for absolute drive path (e.g. C:\...) return filename @@ -126,12 +131,12 @@ except ImportError: # check for absolute UNC path (e.g. \\server\...) return filename # return path with temp directory prepended - return os.path.expandvars(os.path.join('%TEMP%',filename)) + return os.path.expandvars(os.path.join('%TEMP%', filename)) -class FileCacheObject( CacheObject ): - _struct = struct.Struct('dII') # double and two ints - # timestamp, lifetime, position +class FileCacheObject(CacheObject): + _struct = struct.Struct('dII') # double and two ints + # timestamp, lifetime, position @classmethod def fromFile(cls, fd): @@ -150,7 +155,7 @@ class FileCacheObject( CacheObject ): @property def size(self): if self._size is None: - self._buff.seek(0,2) + self._buff.seek(0, 2) size = self._buff.tell() if size == 0: if (self._key is None) or (self._data is None): @@ -159,8 +164,10 @@ class FileCacheObject( CacheObject ): self._size = self._buff.tell() self._size = size return self._size + @size.setter - def size(self, value): self._size = value + def size(self, value): + self._size = value @property def key(self): @@ -170,16 +177,20 @@ class FileCacheObject( CacheObject ): except: pass return self._key + @key.setter - def key(self, value): self._key = value + def key(self, value): + self._key = value @property def data(self): if self._data is None: self._key, self._data = json.loads(self._buff.getvalue()) return self._data + @data.setter - def data(self, value): self._data = value + def data(self, value): + self._data = value def load(self, fd): fd.seek(self.position) @@ -199,7 +210,7 @@ class FileCacheObject( CacheObject ): class FileEngine( CacheEngine ): """Simple file-backed engine.""" name = 'file' - _struct = struct.Struct('HH') # two shorts for version and count + _struct = struct.Struct('HH') # two shorts for version and count _version = 2 def __init__(self, parent): @@ -219,7 +230,6 @@ class FileEngine( CacheEngine ): if self.cachefile is None: raise TMDBCacheError("No cache filename given.") - self.cachefile = parse_filename(self.cachefile) try: @@ -246,7 +256,7 @@ class FileEngine( CacheEngine ): else: # let the unhandled error continue through raise - elif e.errno == errno.EACCESS: + elif e.errno == errno.EACCES: # file exists, but we do not have permission to access it raise TMDBCacheReadError(self.cachefile) else: @@ -257,7 +267,7 @@ class FileEngine( CacheEngine ): self._init_cache() self._open('r+b') - with Flock(self.cachefd, Flock.LOCK_SH): # lock for shared access + with Flock(self.cachefd, Flock.LOCK_SH): # return any new objects in the cache return self._read(date) @@ -265,7 +275,7 @@ class FileEngine( CacheEngine ): self._init_cache() self._open('r+b') - with Flock(self.cachefd, Flock.LOCK_EX): # lock for exclusive access + with Flock(self.cachefd, Flock.LOCK_EX): newobjs = self._read(self.age) newobjs.append(FileCacheObject(key, value, lifetime)) @@ -283,7 +293,8 @@ class FileEngine( CacheEngine ): # already opened in requested mode, nothing to do self.cachefd.seek(0) return - except: pass # catch issue of no cachefile yet opened + except: + pass # catch issue of no cachefile yet opened self.cachefd = io.open(self.cachefile, mode) def _read(self, date): @@ -310,7 +321,7 @@ class FileEngine( CacheEngine ): return [] # get end of file - self.cachefd.seek(0,2) + self.cachefd.seek(0, 2) position = self.cachefd.tell() newobjs = [] emptycount = 0 @@ -348,7 +359,7 @@ class FileEngine( CacheEngine ): data = data[-1] # determine write position of data in cache - self.cachefd.seek(0,2) + self.cachefd.seek(0, 2) end = self.cachefd.tell() data.position = end @@ -387,5 +398,3 @@ class FileEngine( CacheEngine ): def expire(self, key): pass - - diff --git a/libs/tmdb3/cache_null.py b/libs/tmdb3/cache_null.py index a59741c4..8c360da8 100755 --- a/libs/tmdb3/cache_null.py +++ b/libs/tmdb3/cache_null.py @@ -9,11 +9,19 @@ from cache_engine import CacheEngine -class NullEngine( CacheEngine ): + +class NullEngine(CacheEngine): """Non-caching engine for debugging.""" name = 'null' - def configure(self): pass - def get(self, date): return [] - def put(self, key, value, lifetime): return [] - def expire(self, key): pass + def configure(self): + pass + + def get(self, date): + return [] + + def put(self, key, value, lifetime): + return [] + + def expire(self, key): + pass diff --git a/libs/tmdb3/locales.py b/libs/tmdb3/locales.py index 97efec72..0ef0310b 100755 --- a/libs/tmdb3/locales.py +++ b/libs/tmdb3/locales.py @@ -11,7 +11,8 @@ import locale syslocale = None -class LocaleBase( object ): + +class LocaleBase(object): __slots__ = ['__immutable'] _stored = {} fallthrough = False @@ -24,19 +25,21 @@ class LocaleBase( object ): def __setattr__(self, key, value): if getattr(self, '__immutable', False): raise NotImplementedError(self.__class__.__name__ + - ' does not support modification.') + ' does not support modification.') super(LocaleBase, self).__setattr__(key, value) def __delattr__(self, key): if getattr(self, '__immutable', False): raise NotImplementedError(self.__class__.__name__ + - ' does not support modification.') + ' does not support modification.') super(LocaleBase, self).__delattr__(key) def __lt__(self, other): return (id(self) != id(other)) and (str(self) > str(other)) + def __gt__(self, other): return (id(self) != id(other)) and (str(self) < str(other)) + def __eq__(self, other): return (id(self) == id(other)) or (str(self) == str(other)) @@ -48,9 +51,10 @@ class LocaleBase( object ): return cls._stored[key.lower()] except: raise TMDBLocaleError("'{0}' is not a known valid {1} code."\ - .format(key, cls.__name__)) + .format(key, cls.__name__)) -class Language( LocaleBase ): + +class Language(LocaleBase): __slots__ = ['ISO639_1', 'ISO639_2', 'ISO639_2B', 'englishname', 'nativename'] _stored = {} @@ -69,12 +73,13 @@ class Language( LocaleBase ): def __repr__(self): return u"".format(self) -class Country( LocaleBase ): + +class Country(LocaleBase): __slots__ = ['alpha2', 'name'] _stored = {} def __init__(self, alpha2, name): - self.alpha2 = alpha2 + self.alpha2 = alpha2 self.name = name super(Country, self).__init__(alpha2) @@ -84,7 +89,8 @@ class Country( LocaleBase ): def __repr__(self): return u"".format(self) -class Locale( LocaleBase ): + +class Locale(LocaleBase): __slots__ = ['language', 'country', 'encoding'] def __init__(self, language, country, encoding): @@ -120,6 +126,7 @@ class Locale( LocaleBase ): # just return unmodified and hope for the best return dat + def set_locale(language=None, country=None, fallthrough=False): global syslocale LocaleBase.fallthrough = fallthrough @@ -142,6 +149,7 @@ def set_locale(language=None, country=None, fallthrough=False): syslocale = Locale(language, country, sysenc) + def get_locale(language=-1, country=-1): """Output locale using provided attributes, or return system locale.""" global syslocale diff --git a/libs/tmdb3/pager.py b/libs/tmdb3/pager.py index 6cb874c0..ebcb9d2f 100755 --- a/libs/tmdb3/pager.py +++ b/libs/tmdb3/pager.py @@ -8,7 +8,8 @@ from collections import Sequence, Iterator -class PagedIterator( Iterator ): + +class PagedIterator(Iterator): def __init__(self, parent): self._parent = parent self._index = -1 @@ -23,7 +24,8 @@ class PagedIterator( Iterator ): raise StopIteration return self._parent[self._index] -class UnpagedData( object ): + +class UnpagedData(object): def copy(self): return self.__class__() @@ -33,10 +35,11 @@ class UnpagedData( object ): def __rmul__(self, other): return (self.copy() for a in range(other)) -class PagedList( Sequence ): + +class PagedList(Sequence): """ - List-like object, with support for automatically grabbing additional - pages from a data source. + List-like object, with support for automatically grabbing + additional pages from a data source. """ _iter_class = None @@ -87,17 +90,19 @@ class PagedList( Sequence ): pagestart += 1 def _getpage(self, page): - raise NotImplementedError("PagedList._getpage() must be provided "+\ + raise NotImplementedError("PagedList._getpage() must be provided " + "by subclass") -class PagedRequest( PagedList ): + +class PagedRequest(PagedList): """ - Derived PageList that provides a list-like object with automatic paging - intended for use with search requests. + Derived PageList that provides a list-like object with automatic + paging intended for use with search requests. """ def __init__(self, request, handler=None): self._request = request - if handler: self._handler = handler + if handler: + self._handler = handler super(PagedRequest, self).__init__(self._getpage(1), 20) def _getpage(self, page): @@ -105,5 +110,7 @@ class PagedRequest( PagedList ): res = req.readJSON() self._len = res['total_results'] for item in res['results']: - yield self._handler(item) - + if item is None: + yield None + else: + yield self._handler(item) diff --git a/libs/tmdb3/request.py b/libs/tmdb3/request.py index 109630d4..2d51dcd9 100755 --- a/libs/tmdb3/request.py +++ b/libs/tmdb3/request.py @@ -15,6 +15,7 @@ from cache import Cache from urllib import urlencode import urllib2 import json +import os DEBUG = False cache = Cache(filename='pytmdb3.cache') @@ -22,10 +23,11 @@ cache = Cache(filename='pytmdb3.cache') #DEBUG = True #cache = Cache(engine='null') + def set_key(key): """ - Specify the API key to use retrieving data from themoviedb.org. This - key must be set before any calls will function. + Specify the API key to use retrieving data from themoviedb.org. + This key must be set before any calls will function. """ if len(key) != 32: raise TMDBKeyInvalid("Specified API key must be 128-bit hex") @@ -35,42 +37,50 @@ def set_key(key): raise TMDBKeyInvalid("Specified API key must be 128-bit hex") Request._api_key = key + def set_cache(engine=None, *args, **kwargs): """Specify caching engine and properties.""" cache.configure(engine, *args, **kwargs) -class Request( urllib2.Request ): + +class Request(urllib2.Request): _api_key = None _base_url = "http://api.themoviedb.org/3/" @property def api_key(self): if self._api_key is None: - raise TMDBKeyMissing("API key must be specified before "+\ + raise TMDBKeyMissing("API key must be specified before " + "requests can be made") return self._api_key def __init__(self, url, **kwargs): - """Return a request object, using specified API path and arguments.""" + """ + Return a request object, using specified API path and + arguments. + """ kwargs['api_key'] = self.api_key self._url = url.lstrip('/') - self._kwargs = dict([(kwa,kwv) for kwa,kwv in kwargs.items() + self._kwargs = dict([(kwa, kwv) for kwa, kwv in kwargs.items() if kwv is not None]) locale = get_locale() kwargs = {} - for k,v in self._kwargs.items(): + for k, v in self._kwargs.items(): kwargs[k] = locale.encode(v) - url = '{0}{1}?{2}'.format(self._base_url, self._url, urlencode(kwargs)) + url = '{0}{1}?{2}'\ + .format(self._base_url, self._url, urlencode(kwargs)) urllib2.Request.__init__(self, url) self.add_header('Accept', 'application/json') - self.lifetime = 3600 # 1hr + self.lifetime = 3600 # 1hr def new(self, **kwargs): - """Create a new instance of the request, with tweaked arguments.""" + """ + Create a new instance of the request, with tweaked arguments. + """ args = dict(self._kwargs) - for k,v in kwargs.items(): + for k, v in kwargs.items(): if v is None: if k in args: del args[k] @@ -119,35 +129,35 @@ class Request( urllib2.Request ): # no error from TMDB, just raise existing error raise e handle_status(data, url) - #if DEBUG: - # import pprint - # pprint.PrettyPrinter().pprint(data) + if DEBUG: + import pprint + pprint.PrettyPrinter().pprint(data) return data status_handlers = { 1: None, 2: TMDBRequestInvalid('Invalid service - This service does not exist.'), - 3: TMDBRequestError('Authentication Failed - You do not have '+\ + 3: TMDBRequestError('Authentication Failed - You do not have ' + 'permissions to access this service.'), - 4: TMDBRequestInvalid("Invalid format - This service doesn't exist "+\ + 4: TMDBRequestInvalid("Invalid format - This service doesn't exist " + 'in that format.'), - 5: TMDBRequestInvalid('Invalid parameters - Your request parameters '+\ + 5: TMDBRequestInvalid('Invalid parameters - Your request parameters ' + 'are incorrect.'), - 6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid '+\ + 6: TMDBRequestInvalid('Invalid id - The pre-requisite id is invalid ' + 'or not found.'), 7: TMDBKeyInvalid('Invalid API key - You must be granted a valid key.'), - 8: TMDBRequestError('Duplicate entry - The data you tried to submit '+\ + 8: TMDBRequestError('Duplicate entry - The data you tried to submit ' + 'already exists.'), 9: TMDBOffline('This service is tempirarily offline. Try again later.'), - 10: TMDBKeyRevoked('Suspended API key - Access to your account has been '+\ - 'suspended, contact TMDB.'), - 11: TMDBError('Internal error - Something went wrong. Contact TMDb.'), - 12: None, - 13: None, - 14: TMDBRequestError('Authentication Failed.'), - 15: TMDBError('Failed'), - 16: TMDBError('Device Denied'), - 17: TMDBError('Session Denied')} + 10: TMDBKeyRevoked('Suspended API key - Access to your account has been ' + + 'suspended, contact TMDB.'), + 11: TMDBError('Internal error - Something went wrong. Contact TMDb.'), + 12: None, + 13: None, + 14: TMDBRequestError('Authentication Failed.'), + 15: TMDBError('Failed'), + 16: TMDBError('Device Denied'), + 17: TMDBError('Session Denied')} def handle_status(data, query): status = status_handlers[data.get('status_code', 1)] diff --git a/libs/tmdb3/tmdb_api.py b/libs/tmdb3/tmdb_api.py index b5cb0a90..1c8fabd5 100755 --- a/libs/tmdb3/tmdb_api.py +++ b/libs/tmdb3/tmdb_api.py @@ -13,8 +13,8 @@ # (http://creativecommons.org/licenses/GPL/2.0/) #----------------------- -__title__ = "tmdb_api - Simple-to-use Python interface to TMDB's API v3 "+\ - "(www.themoviedb.org)" +__title__ = ("tmdb_api - Simple-to-use Python interface to TMDB's API v3 " + + "(www.themoviedb.org)") __author__ = "Raymond Wagner" __purpose__ = """ This Python library is intended to provide a series of classes and methods @@ -22,7 +22,7 @@ for search and retrieval of text metadata and image URLs from TMDB. Preliminary API specifications can be found at http://help.themoviedb.org/kb/api/about-3""" -__version__="v0.6.17" +__version__ = "v0.7.0" # 0.1.0 Initial development # 0.2.0 Add caching mechanism for API queries # 0.2.1 Temporary work around for broken search paging @@ -59,8 +59,9 @@ __version__="v0.6.17" # 0.6.14 Add support for Lists # 0.6.15 Add ability to search Collections # 0.6.16 Make absent primary images return None (previously u'') -# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove +# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove # releasedate sorting from Collection Movies +# 0.7.0 Add support for television series data from request import set_key, Request from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr @@ -69,10 +70,14 @@ from locales import get_locale, set_locale from tmdb_auth import get_session, set_session from tmdb_exceptions import * +import json +import urllib +import urllib2 import datetime DEBUG = False + def process_date(datestr): try: return datetime.date(*[int(x) for x in datestr.split('-')]) @@ -82,34 +87,40 @@ def process_date(datestr): import traceback _,_,tb = sys.exc_info() f,l,_,_ = traceback.extract_tb(tb)[-1] - warnings.warn_explicit(('"{0}" is not a supported date format. ' - 'Please fix upstream data at http://www.themoviedb.org.')\ - .format(datestr), Warning, f, l) + warnings.warn_explicit(('"{0}" is not a supported date format. ' + + 'Please fix upstream data at ' + + 'http://www.themoviedb.org.' + ).format(datestr), Warning, f, l) return None -class Configuration( Element ): + +class Configuration(Element): images = Datapoint('images') + def _populate(self): return Request('configuration') + Configuration = Configuration() -class Account( NameRepr, Element ): + +class Account(NameRepr, Element): def _populate(self): return Request('account', session_id=self._session.sessionid) - id = Datapoint('id') - adult = Datapoint('include_adult') - country = Datapoint('iso_3166_1') - language = Datapoint('iso_639_1') - name = Datapoint('name') - username = Datapoint('username') + id = Datapoint('id') + adult = Datapoint('include_adult') + country = Datapoint('iso_3166_1') + language = Datapoint('iso_639_1') + name = Datapoint('name') + username = Datapoint('username') @property def locale(self): return get_locale(self.language, self.country) + def searchMovie(query, locale=None, adult=False, year=None): - kwargs = {'query':query, 'include_adult':adult} + kwargs = {'query': query, 'include_adult': adult} if year is not None: try: kwargs['year'] = year.year @@ -117,6 +128,7 @@ def searchMovie(query, locale=None, adult=False, year=None): kwargs['year'] = year return MovieSearchResult(Request('search/movie', **kwargs), locale=locale) + def searchMovieWithYear(query, locale=None, adult=False): year = None if (len(query) > 6) and (query[-1] == ')') and (query[-6] == '('): @@ -134,70 +146,95 @@ def searchMovieWithYear(query, locale=None, adult=False): year = None return searchMovie(query, locale, adult, year) -class MovieSearchResult( SearchRepr, PagedRequest ): + +class MovieSearchResult(SearchRepr, PagedRequest): """Stores a list of search matches.""" _name = None def __init__(self, request, locale=None): if locale is None: locale = get_locale() super(MovieSearchResult, self).__init__( - request.new(language=locale.language), - lambda x: Movie(raw=x, locale=locale)) + request.new(language=locale.language), + lambda x: Movie(raw=x, locale=locale)) + +def searchSeries(query, first_air_date_year=None, search_type=None, locale=None): + return SeriesSearchResult( + Request('search/tv', query=query, first_air_date_year=first_air_date_year, search_type=search_type), + locale=locale) + + +class SeriesSearchResult(SearchRepr, PagedRequest): + """Stores a list of search matches.""" + _name = None + def __init__(self, request, locale=None): + if locale is None: + locale = get_locale() + super(SeriesSearchResult, self).__init__( + request.new(language=locale.language), + lambda x: Series(raw=x, locale=locale)) def searchPerson(query, adult=False): return PeopleSearchResult(Request('search/person', query=query, include_adult=adult)) -class PeopleSearchResult( SearchRepr, PagedRequest ): + +class PeopleSearchResult(SearchRepr, PagedRequest): """Stores a list of search matches.""" _name = None def __init__(self, request): - super(PeopleSearchResult, self).__init__(request, - lambda x: Person(raw=x)) + super(PeopleSearchResult, self).__init__( + request, lambda x: Person(raw=x)) + def searchStudio(query): return StudioSearchResult(Request('search/company', query=query)) -class StudioSearchResult( SearchRepr, PagedRequest ): + +class StudioSearchResult(SearchRepr, PagedRequest): """Stores a list of search matches.""" _name = None def __init__(self, request): - super(StudioSearchResult, self).__init__(request, - lambda x: Studio(raw=x)) + super(StudioSearchResult, self).__init__( + request, lambda x: Studio(raw=x)) + def searchList(query, adult=False): ListSearchResult(Request('search/list', query=query, include_adult=adult)) -class ListSearchResult( SearchRepr, PagedRequest ): + +class ListSearchResult(SearchRepr, PagedRequest): """Stores a list of search matches.""" _name = None def __init__(self, request): - super(ListSearchResult, self).__init__(request, - lambda x: List(raw=x)) + super(ListSearchResult, self).__init__( + request, lambda x: List(raw=x)) + def searchCollection(query, locale=None): return CollectionSearchResult(Request('search/collection', query=query), locale=locale) -class CollectionSearchResult( SearchRepr, PagedRequest ): + +class CollectionSearchResult(SearchRepr, PagedRequest): """Stores a list of search matches.""" _name=None def __init__(self, request, locale=None): if locale is None: locale = get_locale() super(CollectionSearchResult, self).__init__( - request.new(language=locale.language), - lambda x: Collection(raw=x, locale=locale)) + request.new(language=locale.language), + lambda x: Collection(raw=x, locale=locale)) -class Image( Element ): - filename = Datapoint('file_path', initarg=1, - handler=lambda x: x.lstrip('/')) - aspectratio = Datapoint('aspect_ratio') - height = Datapoint('height') - width = Datapoint('width') - language = Datapoint('iso_639_1') - userrating = Datapoint('vote_average') - votes = Datapoint('vote_count') + +class Image(Element): + filename = Datapoint('file_path', initarg=1, + handler=lambda x: x.lstrip('/')) + aspectratio = Datapoint('aspect_ratio') + height = Datapoint('height') + width = Datapoint('width') + language = Datapoint('iso_639_1') + userrating = Datapoint('vote_average') + votes = Datapoint('vote_count') def sizes(self): return ['original'] @@ -205,19 +242,28 @@ class Image( Element ): def geturl(self, size='original'): if size not in self.sizes(): raise TMDBImageSizeError - url = Configuration.images['base_url'].rstrip('/') + url = Configuration.images['secure_base_url'].rstrip('/') return url+'/{0}/{1}'.format(size, self.filename) # sort preferring locale's language, but keep remaining ordering consistent def __lt__(self, other): + if not isinstance(other, Image): + return False return (self.language == self._locale.language) \ and (self.language != other.language) + def __gt__(self, other): + if not isinstance(other, Image): + return True return (self.language != other.language) \ and (other.language == self._locale.language) + # direct match for comparison def __eq__(self, other): + if not isinstance(other, Image): + return False return self.filename == other.filename + # special handling for boolean to see if exists def __nonzero__(self): if len(self.filename) == 0: @@ -228,20 +274,28 @@ class Image( Element ): # BASE62 encoded filename, no need to worry about unicode return u"<{0.__class__.__name__} '{0.filename}'>".format(self) -class Backdrop( Image ): + +class Backdrop(Image): def sizes(self): return Configuration.images['backdrop_sizes'] -class Poster( Image ): + + +class Poster(Image): def sizes(self): return Configuration.images['poster_sizes'] -class Profile( Image ): + + +class Profile(Image): def sizes(self): return Configuration.images['profile_sizes'] -class Logo( Image ): + + +class Logo(Image): def sizes(self): return Configuration.images['logo_sizes'] -class AlternateTitle( Element ): + +class AlternateTitle(Element): country = Datapoint('iso_3166_1') title = Datapoint('title') @@ -249,28 +303,31 @@ class AlternateTitle( Element ): def __lt__(self, other): return (self.country == self._locale.country) \ and (self.country != other.country) + def __gt__(self, other): return (self.country != other.country) \ and (other.country == self._locale.country) + def __eq__(self, other): return self.country == other.country def __repr__(self): return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\ - .format(self).encode('utf-8') + .format(self).encode('utf-8') -class Person( Element ): - id = Datapoint('id', initarg=1) - name = Datapoint('name') - biography = Datapoint('biography') - dayofbirth = Datapoint('birthday', default=None, handler=process_date) - dayofdeath = Datapoint('deathday', default=None, handler=process_date) - homepage = Datapoint('homepage') - birthplace = Datapoint('place_of_birth') - profile = Datapoint('profile_path', handler=Profile, \ - raw=False, default=None) - adult = Datapoint('adult') - aliases = Datalist('also_known_as') + +class Person(Element): + id = Datapoint('id', initarg=1) + name = Datapoint('name') + biography = Datapoint('biography') + dayofbirth = Datapoint('birthday', default=None, handler=process_date) + dayofdeath = Datapoint('deathday', default=None, handler=process_date) + homepage = Datapoint('homepage') + birthplace = Datapoint('place_of_birth') + profile = Datapoint('profile_path', handler=Profile, + raw=False, default=None) + adult = Datapoint('adult') + aliases = Datalist('also_known_as') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>"\ @@ -278,55 +335,63 @@ class Person( Element ): def _populate(self): return Request('person/{0}'.format(self.id)) + def _populate_credits(self): - return Request('person/{0}/credits'.format(self.id), \ - language=self._locale.language) + return Request('person/{0}/credits'.format(self.id), + language=self._locale.language) def _populate_images(self): return Request('person/{0}/images'.format(self.id)) - roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), \ - poller=_populate_credits) - crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), \ - poller=_populate_credits) - profiles = Datalist('profiles', handler=Profile, poller=_populate_images) + roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), + poller=_populate_credits) + crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), + poller=_populate_credits) + profiles = Datalist('profiles', handler=Profile, poller=_populate_images) -class Cast( Person ): - character = Datapoint('character') - order = Datapoint('order') + +class Cast(Person): + character = Datapoint('character') + order = Datapoint('order') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\ - .format(self).encode('utf-8') + .format(self).encode('utf-8') -class Crew( Person ): - job = Datapoint('job') - department = Datapoint('department') + +class Crew(Person): + job = Datapoint('job') + department = Datapoint('department') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\ - .format(self).encode('utf-8') + .format(self).encode('utf-8') -class Keyword( Element ): + +class Keyword(Element): id = Datapoint('id') name = Datapoint('name') def __repr__(self): - return u"<{0.__class__.__name__} {0.name}>".format(self).encode('utf-8') + return u"<{0.__class__.__name__} {0.name}>"\ + .format(self).encode('utf-8') -class Release( Element ): - certification = Datapoint('certification') - country = Datapoint('iso_3166_1') - releasedate = Datapoint('release_date', handler=process_date) + +class Release(Element): + certification = Datapoint('certification') + country = Datapoint('iso_3166_1') + releasedate = Datapoint('release_date', handler=process_date) def __repr__(self): return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\ - .format(self).encode('utf-8') + .format(self).encode('utf-8') -class Trailer( Element ): - name = Datapoint('name') - size = Datapoint('size') - source = Datapoint('source') -class YoutubeTrailer( Trailer ): +class Trailer(Element): + name = Datapoint('name') + size = Datapoint('size') + source = Datapoint('source') + + +class YoutubeTrailer(Trailer): def geturl(self): return "http://www.youtube.com/watch?v={0}".format(self.source) @@ -334,8 +399,9 @@ class YoutubeTrailer( Trailer ): # modified BASE64 encoding, no need to worry about unicode return u"<{0.__class__.__name__} '{0.name}'>".format(self) -class AppleTrailer( Element ): - name = Datapoint('name') + +class AppleTrailer(Element): + name = Datapoint('name') sources = Datadict('sources', handler=Trailer, attr='size') def sizes(self): @@ -344,84 +410,91 @@ class AppleTrailer( Element ): def geturl(self, size=None): if size is None: # sort assuming ###p format for now, take largest resolution - size = str(sorted([int(size[:-1]) for size in self.sources])[-1])+'p' + size = str(sorted( + [int(size[:-1]) for size in self.sources] + )[-1]) + 'p' return self.sources[size].source def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>".format(self) -class Translation( Element ): - name = Datapoint('name') - language = Datapoint('iso_639_1') - englishname = Datapoint('english_name') + +class Translation(Element): + name = Datapoint('name') + language = Datapoint('iso_639_1') + englishname = Datapoint('english_name') def __repr__(self): return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\ - .format(self).encode('utf-8') + .format(self).encode('utf-8') -class Genre( NameRepr, Element ): - id = Datapoint('id') - name = Datapoint('name') + +class Genre(NameRepr, Element): + id = Datapoint('id') + name = Datapoint('name') def _populate_movies(self): return Request('genre/{0}/movies'.format(self.id), \ - language=self._locale.language) + language=self._locale.language) @property def movies(self): if 'movies' not in self._data: search = MovieSearchResult(self._populate_movies(), \ - locale=self._locale) + locale=self._locale) search._name = "{0.name} Movies".format(self) self._data['movies'] = search return self._data['movies'] @classmethod def getAll(cls, locale=None): - class GenreList( Element ): + class GenreList(Element): genres = Datalist('genres', handler=Genre) + def _populate(self): return Request('genre/list', language=self._locale.language) return GenreList(locale=locale).genres - -class Studio( NameRepr, Element ): - id = Datapoint('id', initarg=1) - name = Datapoint('name') - description = Datapoint('description') - headquarters = Datapoint('headquarters') - logo = Datapoint('logo_path', handler=Logo, \ - raw=False, default=None) + +class Studio(NameRepr, Element): + id = Datapoint('id', initarg=1) + name = Datapoint('name') + description = Datapoint('description') + headquarters = Datapoint('headquarters') + logo = Datapoint('logo_path', handler=Logo, raw=False, default=None) # FIXME: manage not-yet-defined handlers in a way that will propogate # locale information properly - parent = Datapoint('parent_company', \ - handler=lambda x: Studio(raw=x)) + parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x)) def _populate(self): return Request('company/{0}'.format(self.id)) + def _populate_movies(self): - return Request('company/{0}/movies'.format(self.id), \ - language=self._locale.language) + return Request('company/{0}/movies'.format(self.id), + language=self._locale.language) # FIXME: add a cleaner way of adding types with no additional processing @property def movies(self): if 'movies' not in self._data: - search = MovieSearchResult(self._populate_movies(), \ - locale=self._locale) + search = MovieSearchResult(self._populate_movies(), + locale=self._locale) search._name = "{0.name} Movies".format(self) self._data['movies'] = search return self._data['movies'] -class Country( NameRepr, Element ): - code = Datapoint('iso_3166_1') - name = Datapoint('name') -class Language( NameRepr, Element ): - code = Datapoint('iso_639_1') - name = Datapoint('name') +class Country(NameRepr, Element): + code = Datapoint('iso_3166_1') + name = Datapoint('name') -class Movie( Element ): + +class Language(NameRepr, Element): + code = Datapoint('iso_639_1') + name = Datapoint('name') + + +class Movie(Element): @classmethod def latest(cls): req = Request('latest/movie') @@ -459,7 +532,7 @@ class Movie( Element ): account = Account(session=session) res = MovieSearchResult( Request('account/{0}/favorite_movies'.format(account.id), - session_id=session.sessionid)) + session_id=session.sessionid)) res._name = "Favorites" return res @@ -470,7 +543,7 @@ class Movie( Element ): account = Account(session=session) res = MovieSearchResult( Request('account/{0}/rated_movies'.format(account.id), - session_id=session.sessionid)) + session_id=session.sessionid)) res._name = "Movies You Rated" return res @@ -481,7 +554,7 @@ class Movie( Element ): account = Account(session=session) res = MovieSearchResult( Request('account/{0}/movie_watchlist'.format(account.id), - session_id=session.sessionid)) + session_id=session.sessionid)) res._name = "Movies You're Watching" return res @@ -500,104 +573,116 @@ class Movie( Element ): movie._populate() return movie - id = Datapoint('id', initarg=1) - title = Datapoint('title') - originaltitle = Datapoint('original_title') - tagline = Datapoint('tagline') - overview = Datapoint('overview') - runtime = Datapoint('runtime') - budget = Datapoint('budget') - revenue = Datapoint('revenue') - releasedate = Datapoint('release_date', handler=process_date) - homepage = Datapoint('homepage') - imdb = Datapoint('imdb_id') + id = Datapoint('id', initarg=1) + title = Datapoint('title') + originaltitle = Datapoint('original_title') + tagline = Datapoint('tagline') + overview = Datapoint('overview') + runtime = Datapoint('runtime') + budget = Datapoint('budget') + revenue = Datapoint('revenue') + releasedate = Datapoint('release_date', handler=process_date) + homepage = Datapoint('homepage') + imdb = Datapoint('imdb_id') - backdrop = Datapoint('backdrop_path', handler=Backdrop, \ - raw=False, default=None) - poster = Datapoint('poster_path', handler=Poster, \ - raw=False, default=None) + backdrop = Datapoint('backdrop_path', handler=Backdrop, + raw=False, default=None) + poster = Datapoint('poster_path', handler=Poster, + raw=False, default=None) - popularity = Datapoint('popularity') - userrating = Datapoint('vote_average') - votes = Datapoint('vote_count') + popularity = Datapoint('popularity') + userrating = Datapoint('vote_average') + votes = Datapoint('vote_count') - adult = Datapoint('adult') - collection = Datapoint('belongs_to_collection', handler=lambda x: \ + adult = Datapoint('adult') + collection = Datapoint('belongs_to_collection', handler=lambda x: \ Collection(raw=x)) - genres = Datalist('genres', handler=Genre) - studios = Datalist('production_companies', handler=Studio) - countries = Datalist('production_countries', handler=Country) - languages = Datalist('spoken_languages', handler=Language) + genres = Datalist('genres', handler=Genre) + studios = Datalist('production_companies', handler=Studio) + countries = Datalist('production_countries', handler=Country) + languages = Datalist('spoken_languages', handler=Language) def _populate(self): return Request('movie/{0}'.format(self.id), \ - language=self._locale.language) + language=self._locale.language) + def _populate_titles(self): kwargs = {} if not self._locale.fallthrough: kwargs['country'] = self._locale.country - return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs) + return Request('movie/{0}/alternative_titles'.format(self.id), + **kwargs) + def _populate_cast(self): return Request('movie/{0}/casts'.format(self.id)) + def _populate_images(self): kwargs = {} if not self._locale.fallthrough: kwargs['language'] = self._locale.language return Request('movie/{0}/images'.format(self.id), **kwargs) + def _populate_keywords(self): return Request('movie/{0}/keywords'.format(self.id)) + def _populate_releases(self): return Request('movie/{0}/releases'.format(self.id)) + def _populate_trailers(self): - return Request('movie/{0}/trailers'.format(self.id), \ + return Request('movie/{0}/trailers'.format(self.id), language=self._locale.language) + def _populate_translations(self): return Request('movie/{0}/translations'.format(self.id)) alternate_titles = Datalist('titles', handler=AlternateTitle, \ - poller=_populate_titles, sort=True) - cast = Datalist('cast', handler=Cast, \ - poller=_populate_cast, sort='order') - crew = Datalist('crew', handler=Crew, poller=_populate_cast) - backdrops = Datalist('backdrops', handler=Backdrop, \ - poller=_populate_images, sort=True) - posters = Datalist('posters', handler=Poster, \ - poller=_populate_images, sort=True) - keywords = Datalist('keywords', handler=Keyword, \ - poller=_populate_keywords) - releases = Datadict('countries', handler=Release, \ - poller=_populate_releases, attr='country') - youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, \ - poller=_populate_trailers) - apple_trailers = Datalist('quicktime', handler=AppleTrailer, \ - poller=_populate_trailers) - translations = Datalist('translations', handler=Translation, \ - poller=_populate_translations) + poller=_populate_titles, sort=True) + + # FIXME: this data point will need to be changed to 'credits' at some point + cast = Datalist('cast', handler=Cast, + poller=_populate_cast, sort='order') + + crew = Datalist('crew', handler=Crew, poller=_populate_cast) + backdrops = Datalist('backdrops', handler=Backdrop, + poller=_populate_images, sort=True) + posters = Datalist('posters', handler=Poster, + poller=_populate_images, sort=True) + keywords = Datalist('keywords', handler=Keyword, + poller=_populate_keywords) + releases = Datadict('countries', handler=Release, + poller=_populate_releases, attr='country') + youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, + poller=_populate_trailers) + apple_trailers = Datalist('quicktime', handler=AppleTrailer, + poller=_populate_trailers) + translations = Datalist('translations', handler=Translation, + poller=_populate_translations) def setFavorite(self, value): - req = Request('account/{0}/favorite'.format(\ - Account(session=self._session).id), - session_id=self._session.sessionid) - req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()}) + req = Request('account/{0}/favorite'.format( + Account(session=self._session).id), + session_id=self._session.sessionid) + req.add_data({'movie_id': self.id, + 'favorite': str(bool(value)).lower()}) req.lifetime = 0 req.readJSON() def setRating(self, value): if not (0 <= value <= 10): raise TMDBError("Ratings must be between '0' and '10'.") - req = Request('movie/{0}/rating'.format(self.id), \ - session_id=self._session.sessionid) + req = Request('movie/{0}/rating'.format(self.id), + session_id=self._session.sessionid) req.lifetime = 0 req.add_data({'value':value}) req.readJSON() def setWatchlist(self, value): - req = Request('account/{0}/movie_watchlist'.format(\ - Account(session=self._session).id), - session_id=self._session.sessionid) + req = Request('account/{0}/movie_watchlist'.format( + Account(session=self._session).id), + session_id=self._session.sessionid) req.lifetime = 0 - req.add_data({'movie_id':self.id, - 'movie_watchlist':str(bool(value)).lower()}) + req.add_data({'movie_id': self.id, + 'movie_watchlist': str(bool(value)).lower()}) req.readJSON() def getSimilar(self): @@ -605,9 +690,9 @@ class Movie( Element ): @property def similar(self): - res = MovieSearchResult(Request('movie/{0}/similar_movies'\ - .format(self.id)), - locale=self._locale) + res = MovieSearchResult(Request( + 'movie/{0}/similar_movies'.format(self.id)), + locale=self._locale) res._name = 'Similar to {0}'.format(self._printable_name()) return res @@ -629,61 +714,197 @@ class Movie( Element ): return s def __repr__(self): - return u"<{0} {1}>".format(self.__class__.__name__,\ + return u"<{0} {1}>".format(self.__class__.__name__, self._printable_name()).encode('utf-8') + class ReverseCast( Movie ): - character = Datapoint('character') + character = Datapoint('character') def __repr__(self): - return u"<{0.__class__.__name__} '{0.character}' on {1}>"\ - .format(self, self._printable_name()).encode('utf-8') + return (u"<{0.__class__.__name__} '{0.character}' on {1}>" + .format(self, self._printable_name()).encode('utf-8')) + class ReverseCrew( Movie ): - department = Datapoint('department') - job = Datapoint('job') + department = Datapoint('department') + job = Datapoint('job') def __repr__(self): - return u"<{0.__class__.__name__} '{0.job}' for {1}>"\ - .format(self, self._printable_name()).encode('utf-8') + return (u"<{0.__class__.__name__} '{0.job}' for {1}>" + .format(self, self._printable_name()).encode('utf-8')) -class Collection( NameRepr, Element ): - id = Datapoint('id', initarg=1) - name = Datapoint('name') + +class Collection(NameRepr, Element): + id = Datapoint('id', initarg=1) + name = Datapoint('name') backdrop = Datapoint('backdrop_path', handler=Backdrop, \ - raw=False, default=None) - poster = Datapoint('poster_path', handler=Poster, \ - raw=False, default=None) - members = Datalist('parts', handler=Movie) + raw=False, default=None) + poster = Datapoint('poster_path', handler=Poster, raw=False, default=None) + members = Datalist('parts', handler=Movie) overview = Datapoint('overview') def _populate(self): - return Request('collection/{0}'.format(self.id), \ - language=self._locale.language) + return Request('collection/{0}'.format(self.id), + language=self._locale.language) + def _populate_images(self): kwargs = {} if not self._locale.fallthrough: kwargs['language'] = self._locale.language return Request('collection/{0}/images'.format(self.id), **kwargs) - backdrops = Datalist('backdrops', handler=Backdrop, \ - poller=_populate_images, sort=True) - posters = Datalist('posters', handler=Poster, \ - poller=_populate_images, sort=True) + backdrops = Datalist('backdrops', handler=Backdrop, + poller=_populate_images, sort=True) + posters = Datalist('posters', handler=Poster, + poller=_populate_images, sort=True) -class List( NameRepr, Element ): - id = Datapoint('id', initarg=1) - name = Datapoint('name') - author = Datapoint('created_by') +class List(NameRepr, Element): + id = Datapoint('id', initarg=1) + name = Datapoint('name') + author = Datapoint('created_by') description = Datapoint('description') - favorites = Datapoint('favorite_count') - language = Datapoint('iso_639_1') - count = Datapoint('item_count') - poster = Datapoint('poster_path', handler=Poster, \ - raw=False, default=None) - - members = Datalist('items', handler=Movie) + favorites = Datapoint('favorite_count') + language = Datapoint('iso_639_1') + count = Datapoint('item_count') + poster = Datapoint('poster_path', handler=Poster, raw=False, default=None) + members = Datalist('items', handler=Movie) def _populate(self): return Request('list/{0}'.format(self.id)) +class Network(NameRepr,Element): + id = Datapoint('id', initarg=1) + name = Datapoint('name') + +class Episode(NameRepr, Element): + episode_number = Datapoint('episode_number', initarg=3) + season_number = Datapoint('season_number', initarg=2) + series_id = Datapoint('series_id', initarg=1) + air_date = Datapoint('air_date', handler=process_date) + overview = Datapoint('overview') + name = Datapoint('name') + userrating = Datapoint('vote_average') + votes = Datapoint('vote_count') + id = Datapoint('id') + production_code = Datapoint('production_code') + still = Datapoint('still_path', handler=Backdrop, raw=False, default=None) + + def _populate(self): + return Request('tv/{0}/season/{1}/episode/{2}'.format(self.series_id, self.season_number, self.episode_number), + language=self._locale.language) + + def _populate_cast(self): + return Request('tv/{0}/season/{1}/episode/{2}/credits'.format( + self.series_id, self.season_number, self.episode_number), + language=self._locale.language) + + def _populate_external_ids(self): + return Request('tv/{0}/season/{1}/episode/{2}/external_ids'.format( + self.series_id, self.season_number, self.episode_number)) + + def _populate_images(self): + kwargs = {} + if not self._locale.fallthrough: + kwargs['language'] = self._locale.language + return Request('tv/{0}/season/{1}/episode/{2}/images'.format( + self.series_id, self.season_number, self.episode_number), **kwargs) + + cast = Datalist('cast', handler=Cast, + poller=_populate_cast, sort='order') + guest_stars = Datalist('guest_stars', handler=Cast, + poller=_populate_cast, sort='order') + crew = Datalist('crew', handler=Crew, poller=_populate_cast) + imdb_id = Datapoint('imdb_id', poller=_populate_external_ids) + freebase_id = Datapoint('freebase_id', poller=_populate_external_ids) + freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids) + tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids) + tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids) + stills = Datalist('stills', handler=Backdrop, poller=_populate_images, sort=True) + +class Season(NameRepr, Element): + season_number = Datapoint('season_number', initarg=2) + series_id = Datapoint('series_id', initarg=1) + id = Datapoint('id') + air_date = Datapoint('air_date', handler=process_date) + poster = Datapoint('poster_path', handler=Poster, raw=False, default=None) + overview = Datapoint('overview') + name = Datapoint('name') + episodes = Datadict('episodes', attr='episode_number', handler=Episode, + passthrough={'series_id': 'series_id', 'season_number': 'season_number'}) + + def _populate(self): + return Request('tv/{0}/season/{1}'.format(self.series_id, self.season_number), + language=self._locale.language) + + def _populate_images(self): + kwargs = {} + if not self._locale.fallthrough: + kwargs['language'] = self._locale.language + return Request('tv/{0}/season/{1}/images'.format(self.series_id, self.season_number), **kwargs) + + def _populate_external_ids(self): + return Request('tv/{0}/season/{1}/external_ids'.format(self.series_id, self.season_number)) + + posters = Datalist('posters', handler=Poster, + poller=_populate_images, sort=True) + + freebase_id = Datapoint('freebase_id', poller=_populate_external_ids) + freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids) + tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids) + tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids) + +class Series(NameRepr, Element): + id = Datapoint('id', initarg=1) + backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None) + authors = Datalist('created_by', handler=Person) + episode_run_times = Datalist('episode_run_time') + first_air_date = Datapoint('first_air_date', handler=process_date) + last_air_date = Datapoint('last_air_date', handler=process_date) + genres = Datalist('genres', handler=Genre) + homepage = Datapoint('homepage') + in_production = Datapoint('in_production') + languages = Datalist('languages') + origin_countries = Datalist('origin_country') + name = Datapoint('name') + original_name = Datapoint('original_name') + number_of_episodes = Datapoint('number_of_episodes') + number_of_seasons = Datapoint('number_of_seasons') + overview = Datapoint('overview') + popularity = Datapoint('popularity') + status = Datapoint('status') + userrating = Datapoint('vote_average') + votes = Datapoint('vote_count') + poster = Datapoint('poster_path', handler=Poster, raw=False, default=None) + networks = Datalist('networks', handler=Network) + seasons = Datadict('seasons', attr='season_number', handler=Season, passthrough={'id': 'series_id'}) + + def _populate(self): + return Request('tv/{0}'.format(self.id), + language=self._locale.language) + + def _populate_cast(self): + return Request('tv/{0}/credits'.format(self.id)) + + def _populate_images(self): + kwargs = {} + if not self._locale.fallthrough: + kwargs['language'] = self._locale.language + return Request('tv/{0}/images'.format(self.id), **kwargs) + + def _populate_external_ids(self): + return Request('tv/{0}/external_ids'.format(self.id)) + + cast = Datalist('cast', handler=Cast, + poller=_populate_cast, sort='order') + crew = Datalist('crew', handler=Crew, poller=_populate_cast) + backdrops = Datalist('backdrops', handler=Backdrop, + poller=_populate_images, sort=True) + posters = Datalist('posters', handler=Poster, + poller=_populate_images, sort=True) + + imdb_id = Datapoint('imdb_id', poller=_populate_external_ids) + freebase_id = Datapoint('freebase_id', poller=_populate_external_ids) + freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids) + tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids) + tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids) diff --git a/libs/tmdb3/tmdb_auth.py b/libs/tmdb3/tmdb_auth.py index 8583b990..b447b5ab 100755 --- a/libs/tmdb3/tmdb_auth.py +++ b/libs/tmdb3/tmdb_auth.py @@ -11,7 +11,7 @@ from datetime import datetime as _pydatetime, \ tzinfo as _pytzinfo import re -class datetime( _pydatetime ): +class datetime(_pydatetime): """Customized datetime class with ISO format parsing.""" _reiso = re.compile('(?P[0-9]{4})' '-(?P[0-9]{1,2})' @@ -27,21 +27,27 @@ class datetime( _pydatetime ): '(?P[0-9]{2})?' ')?') - class _tzinfo( _pytzinfo): + class _tzinfo(_pytzinfo): def __init__(self, direc='+', hr=0, min=0): if direc == '-': hr = -1*int(hr) self._offset = timedelta(hours=int(hr), minutes=int(min)) - def utcoffset(self, dt): return self._offset - def tzname(self, dt): return '' - def dst(self, dt): return timedelta(0) + + def utcoffset(self, dt): + return self._offset + + def tzname(self, dt): + return '' + + def dst(self, dt): + return timedelta(0) @classmethod def fromIso(cls, isotime, sep='T'): match = cls._reiso.match(isotime) if match is None: - raise TypeError("time data '%s' does not match ISO 8601 format" \ - % isotime) + raise TypeError("time data '%s' does not match ISO 8601 format" + % isotime) dt = [int(a) for a in match.groups()[:5]] if match.group('sec') is not None: @@ -52,9 +58,9 @@ class datetime( _pydatetime ): if match.group('tz') == 'Z': tz = cls._tzinfo() elif match.group('tzmin'): - tz = cls._tzinfo(*match.group('tzdirec','tzhour','tzmin')) + tz = cls._tzinfo(*match.group('tzdirec', 'tzhour', 'tzmin')) else: - tz = cls._tzinfo(*match.group('tzdirec','tzhour')) + tz = cls._tzinfo(*match.group('tzdirec', 'tzhour')) dt.append(0) dt.append(tz) return cls(*dt) @@ -64,10 +70,12 @@ from tmdb_exceptions import * syssession = None + def set_session(sessionid): global syssession syssession = Session(sessionid) + def get_session(sessionid=None): global syssession if sessionid: @@ -77,8 +85,8 @@ def get_session(sessionid=None): else: return Session.new() -class Session( object ): +class Session(object): @classmethod def new(cls): return cls(None) @@ -91,9 +99,9 @@ class Session( object ): if self._sessionid is None: if self._authtoken is None: raise TMDBError("No Auth Token to produce Session for") - # TODO: check authtokenexpiration against current time - req = Request('authentication/session/new', \ - request_token=self._authtoken) + # TODO: check authtoken expiration against current time + req = Request('authentication/session/new', + request_token=self._authtoken) req.lifetime = 0 dat = req.readJSON() if not dat['success']: @@ -128,4 +136,3 @@ class Session( object ): @property def callbackurl(self): return "http://www.themoviedb.org/authenticate/"+self._authtoken - diff --git a/libs/tmdb3/tmdb_exceptions.py b/libs/tmdb3/tmdb_exceptions.py index 35e0364b..f85fbcf4 100755 --- a/libs/tmdb3/tmdb_exceptions.py +++ b/libs/tmdb3/tmdb_exceptions.py @@ -6,23 +6,24 @@ # Author: Raymond Wagner #----------------------- -class TMDBError( Exception ): - Error = 0 - KeyError = 10 - KeyMissing = 20 - KeyInvalid = 30 - KeyRevoked = 40 - RequestError = 50 - RequestInvalid = 51 - PagingIssue = 60 - CacheError = 70 - CacheReadError = 71 - CacheWriteError = 72 - CacheDirectoryError = 73 - ImageSizeError = 80 - HTTPError = 90 - Offline = 100 - LocaleError = 110 + +class TMDBError(Exception): + Error = 0 + KeyError = 10 + KeyMissing = 20 + KeyInvalid = 30 + KeyRevoked = 40 + RequestError = 50 + RequestInvalid = 51 + PagingIssue = 60 + CacheError = 70 + CacheReadError = 71 + CacheWriteError = 72 + CacheDirectoryError = 73 + ImageSizeError = 80 + HTTPError = 90 + Offline = 100 + LocaleError = 110 def __init__(self, msg=None, errno=0): self.errno = errno @@ -30,60 +31,77 @@ class TMDBError( Exception ): self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno) self.args = (msg,) -class TMDBKeyError( TMDBError ): + +class TMDBKeyError(TMDBError): pass -class TMDBKeyMissing( TMDBKeyError ): + +class TMDBKeyMissing(TMDBKeyError): pass -class TMDBKeyInvalid( TMDBKeyError ): + +class TMDBKeyInvalid(TMDBKeyError): pass -class TMDBKeyRevoked( TMDBKeyInvalid ): + +class TMDBKeyRevoked(TMDBKeyInvalid): pass -class TMDBRequestError( TMDBError ): + +class TMDBRequestError(TMDBError): pass -class TMDBRequestInvalid( TMDBRequestError ): + +class TMDBRequestInvalid(TMDBRequestError): pass -class TMDBPagingIssue( TMDBRequestError ): + +class TMDBPagingIssue(TMDBRequestError): pass -class TMDBCacheError( TMDBRequestError ): + +class TMDBCacheError(TMDBRequestError): pass -class TMDBCacheReadError( TMDBCacheError ): + +class TMDBCacheReadError(TMDBCacheError): def __init__(self, filename): super(TMDBCacheReadError, self).__init__( - "User does not have permission to access cache file: {0}.".format(filename)) + "User does not have permission to access cache file: {0}."\ + .format(filename)) self.filename = filename -class TMDBCacheWriteError( TMDBCacheError ): + +class TMDBCacheWriteError(TMDBCacheError): def __init__(self, filename): super(TMDBCacheWriteError, self).__init__( - "User does not have permission to write cache file: {0}.".format(filename)) + "User does not have permission to write cache file: {0}."\ + .format(filename)) self.filename = filename -class TMDBCacheDirectoryError( TMDBCacheError ): + +class TMDBCacheDirectoryError(TMDBCacheError): def __init__(self, filename): super(TMDBCacheDirectoryError, self).__init__( - "Directory containing cache file does not exist: {0}.".format(filename)) + "Directory containing cache file does not exist: {0}."\ + .format(filename)) self.filename = filename -class TMDBImageSizeError( TMDBError ): + +class TMDBImageSizeError(TMDBError ): pass -class TMDBHTTPError( TMDBError ): + +class TMDBHTTPError(TMDBError): def __init__(self, err): self.httperrno = err.code self.response = err.fp.read() super(TMDBHTTPError, self).__init__(str(err)) -class TMDBOffline( TMDBError ): + +class TMDBOffline(TMDBError): pass -class TMDBLocaleError( TMDBError ): - pass +class TMDBLocaleError(TMDBError): + pass diff --git a/libs/tmdb3/util.py b/libs/tmdb3/util.py index bba9fcc7..a0d2e28a 100755 --- a/libs/tmdb3/util.py +++ b/libs/tmdb3/util.py @@ -10,13 +10,15 @@ from copy import copy from locales import get_locale from tmdb_auth import get_session -class NameRepr( object ): + +class NameRepr(object): """Mixin for __repr__ methods using 'name' attribute.""" def __repr__(self): return u"<{0.__class__.__name__} '{0.name}'>"\ - .format(self).encode('utf-8') + .format(self).encode('utf-8') -class SearchRepr( object ): + +class SearchRepr(object): """ Mixin for __repr__ methods for classes with '_name' and '_request' attributes. @@ -25,10 +27,11 @@ class SearchRepr( object ): name = self._name if self._name else self._request._kwargs['query'] return u"".format(name).encode('utf-8') -class Poller( object ): + +class Poller(object): """ - Wrapper for an optional callable to populate an Element derived class - with raw data, or data from a Request. + Wrapper for an optional callable to populate an Element derived + class with raw data, or data from a Request. """ def __init__(self, func, lookup, inst=None): self.func = func @@ -60,7 +63,7 @@ class Poller( object ): if not callable(self.func): raise RuntimeError('Poller object called without a source function') req = self.func() - if (('language' in req._kwargs) or ('country' in req._kwargs)) \ + if ('language' in req._kwargs) or ('country' in req._kwargs) \ and self.inst._locale.fallthrough: # request specifies a locale filter, and fallthrough is enabled # run a first pass with specified filter @@ -79,7 +82,7 @@ class Poller( object ): def apply(self, data, set_nones=True): # apply data directly, bypassing callable function unfilled = False - for k,v in self.lookup.items(): + for k, v in self.lookup.items(): if (k in data) and \ ((data[k] is not None) if callable(self.func) else True): # argument received data, populate it @@ -100,32 +103,38 @@ class Poller( object ): unfilled = True return unfilled -class Data( object ): + +class Data(object): """ Basic response definition class This maps to a single key in a JSON dictionary received from the API """ def __init__(self, field, initarg=None, handler=None, poller=None, - raw=True, default=u'', lang=False): + raw=True, default=u'', lang=None, passthrough={}): """ - This defines how the dictionary value is to be processed by the poller - field -- defines the dictionary key that filters what data this uses - initarg -- (optional) specifies that this field must be supplied - when creating a new instance of the Element class this - definition is mapped to. Takes an integer for the order - it should be used in the input arguments - handler -- (optional) callable used to process the received value - before being stored in the Element object. - poller -- (optional) callable to be used if data is requested and - this value has not yet been defined. the callable should - return a dictionary of data from a JSON query. many - definitions may share a single poller, which will be - and the data used to populate all referenced definitions - based off their defined field - raw -- (optional) if the specified handler is an Element class, - the data will be passed into it using the 'raw' keyword - attribute. setting this to false will force the data to - instead be passed in as the first argument + This defines how the dictionary value is to be processed by the + poller + field -- defines the dictionary key that filters what data + this uses + initarg -- (optional) specifies that this field must be + supplied when creating a new instance of the Element + class this definition is mapped to. Takes an integer + for the order it should be used in the input + arguments + handler -- (optional) callable used to process the received + value before being stored in the Element object. + poller -- (optional) callable to be used if data is requested + and this value has not yet been defined. the + callable should return a dictionary of data from a + JSON query. many definitions may share a single + poller, which will be and the data used to populate + all referenced definitions based off their defined + field + raw -- (optional) if the specified handler is an Element + class, the data will be passed into it using the + 'raw' keyword attribute. setting this to false + will force the data to instead be passed in as the + first argument """ self.field = field self.initarg = initarg @@ -133,6 +142,7 @@ class Data( object ): self.raw = raw self.default = default self.sethandler(handler) + self.passthrough = passthrough def __get__(self, inst, owner): if inst is None: @@ -151,6 +161,9 @@ class Data( object ): if isinstance(value, Element): value._locale = inst._locale value._session = inst._session + + for source, dest in self.passthrough: + setattr(value, dest, getattr(inst, source)) inst._data[self.field] = value def sethandler(self, handler): @@ -162,37 +175,44 @@ class Data( object ): else: self.handler = lambda x: handler(x) -class Datapoint( Data ): + +class Datapoint(Data): pass -class Datalist( Data ): + +class Datalist(Data): """ Response definition class for list data This maps to a key in a JSON dictionary storing a list of data """ - def __init__(self, field, handler=None, poller=None, sort=None, raw=True): + def __init__(self, field, handler=None, poller=None, sort=None, raw=True, passthrough={}): """ - This defines how the dictionary value is to be processed by the poller - field -- defines the dictionary key that filters what data this uses - handler -- (optional) callable used to process the received value - before being stored in the Element object. - poller -- (optional) callable to be used if data is requested and - this value has not yet been defined. the callable should - return a dictionary of data from a JSON query. many - definitions may share a single poller, which will be - and the data used to populate all referenced definitions - based off their defined field - sort -- (optional) name of attribute in resultant data to be used - to sort the list after processing. this effectively - a handler be defined to process the data into something - that has attributes - raw -- (optional) if the specified handler is an Element class, - the data will be passed into it using the 'raw' keyword - attribute. setting this to false will force the data to - instead be passed in as the first argument + This defines how the dictionary value is to be processed by the + poller + field -- defines the dictionary key that filters what data + this uses + handler -- (optional) callable used to process the received + value before being stored in the Element object. + poller -- (optional) callable to be used if data is requested + and this value has not yet been defined. the + callable should return a dictionary of data from a + JSON query. many definitions may share a single + poller, which will be and the data used to populate + all referenced definitions based off their defined + field + sort -- (optional) name of attribute in resultant data to be + used to sort the list after processing. this + effectively requires a handler be defined to process + the data into something that has attributes + raw -- (optional) if the specified handler is an Element + class, the data will be passed into it using the + 'raw' keyword attribute. setting this to false will + force the data to instead be passed in as the first + argument """ - super(Datalist, self).__init__(field, None, handler, poller, raw) + super(Datalist, self).__init__(field, None, handler, poller, raw, passthrough=passthrough) self.sort = sort + def __set__(self, inst, value): data = [] if value: @@ -201,6 +221,10 @@ class Datalist( Data ): if isinstance(val, Element): val._locale = inst._locale val._session = inst._session + + for source, dest in self.passthrough.items(): + setattr(val, dest, getattr(inst, source)) + data.append(val) if self.sort: if self.sort is True: @@ -209,45 +233,52 @@ class Datalist( Data ): data.sort(key=lambda x: getattr(x, self.sort)) inst._data[self.field] = data -class Datadict( Data ): + +class Datadict(Data): """ Response definition class for dictionary data This maps to a key in a JSON dictionary storing a dictionary of data """ def __init__(self, field, handler=None, poller=None, raw=True, - key=None, attr=None): + key=None, attr=None, passthrough={}): """ - This defines how the dictionary value is to be processed by the poller - field -- defines the dictionary key that filters what data this uses - handler -- (optional) callable used to process the received value - before being stored in the Element object. - poller -- (optional) callable to be used if data is requested and - this value has not yet been defined. the callable should - return a dictionary of data from a JSON query. many - definitions may share a single poller, which will be - and the data used to populate all referenced definitions - based off their defined field - key -- (optional) name of key in resultant data to be used as - the key in the stored dictionary. if this is not the - field name from the source data is used instead - attr -- (optional) name of attribute in resultant data to be used + This defines how the dictionary value is to be processed by the + poller + field -- defines the dictionary key that filters what data + this uses + handler -- (optional) callable used to process the received + value before being stored in the Element object. + poller -- (optional) callable to be used if data is requested + and this value has not yet been defined. the + callable should return a dictionary of data from a + JSON query. many definitions may share a single + poller, which will be and the data used to populate + all referenced definitions based off their defined + field + key -- (optional) name of key in resultant data to be used as the key in the stored dictionary. if this is not the field name from the source data is used instead - raw -- (optional) if the specified handler is an Element class, - the data will be passed into it using the 'raw' keyword - attribute. setting this to false will force the data to - instead be passed in as the first argument + attr -- (optional) name of attribute in resultant data to be + used as the key in the stored dictionary. if this is + not the field name from the source data is used + instead + raw -- (optional) if the specified handler is an Element + class, the data will be passed into it using the + 'raw' keyword attribute. setting this to false will + force the data to instead be passed in as the first + argument """ if key and attr: raise TypeError("`key` and `attr` cannot both be defined") - super(Datadict, self).__init__(field, None, handler, poller, raw) + super(Datadict, self).__init__(field, None, handler, poller, raw, passthrough=passthrough) if key: self.getkey = lambda x: x[key] elif attr: self.getkey = lambda x: getattr(x, attr) else: - raise TypeError("Datadict requires `key` or `attr` be defined "+\ + raise TypeError("Datadict requires `key` or `attr` be defined " + "for populating the dictionary") + def __set__(self, inst, value): data = {} if value: @@ -256,6 +287,10 @@ class Datadict( Data ): if isinstance(val, Element): val._locale = inst._locale val._session = inst._session + + for source, dest in self.passthrough.items(): + setattr(val, dest, getattr(inst, source)) + data[self.getkey(val)] = val inst._data[self.field] = data @@ -286,7 +321,7 @@ class ElementType( type ): # extract copies of each defined Poller function # from parent classes pollers[k] = attr.func - for k,attr in attrs.items(): + for k, attr in attrs.items(): if isinstance(attr, Data): data[k] = attr if '_populate' in attrs: @@ -295,9 +330,9 @@ class ElementType( type ): # process all defined Data attribues, testing for use as an initial # argument, and building a list of what Pollers are used to populate # which Data points - pollermap = dict([(k,[]) for k in pollers]) + pollermap = dict([(k, []) for k in pollers]) initargs = [] - for k,v in data.items(): + for k, v in data.items(): v.name = k if v.initarg: initargs.append(v) @@ -313,7 +348,7 @@ class ElementType( type ): # wrap each used poller function with a Poller class, and push into # the new class attributes - for k,v in pollermap.items(): + for k, v in pollermap.items(): if len(v) == 0: continue lookup = dict([(attr.field, attr.name) for attr in v]) @@ -326,8 +361,8 @@ class ElementType( type ): attrs[attr.name] = attr # build sorted list of arguments used for intialization - attrs['_InitArgs'] = tuple([a.name for a in \ - sorted(initargs, key=lambda x: x.initarg)]) + attrs['_InitArgs'] = tuple( + [a.name for a in sorted(initargs, key=lambda x: x.initarg)]) return type.__new__(mcs, name, bases, attrs) def __call__(cls, *args, **kwargs): @@ -346,21 +381,23 @@ class ElementType( type ): if 'raw' in kwargs: # if 'raw' keyword is supplied, create populate object manually if len(args) != 0: - raise TypeError('__init__() takes exactly 2 arguments (1 given)') + raise TypeError( + '__init__() takes exactly 2 arguments (1 given)') obj._populate.apply(kwargs['raw'], False) else: # if not, the number of input arguments must exactly match that # defined by the Data definitions if len(args) != len(cls._InitArgs): - raise TypeError('__init__() takes exactly {0} arguments ({1} given)'\ + raise TypeError( + '__init__() takes exactly {0} arguments ({1} given)'\ .format(len(cls._InitArgs)+1, len(args)+1)) - for a,v in zip(cls._InitArgs, args): + for a, v in zip(cls._InitArgs, args): setattr(obj, a, v) obj.__init__() return obj + class Element( object ): __metaclass__ = ElementType _lang = 'en' - From 30f5a3944cd98f5454b5d537255a19a642b33695 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 14:11:21 +0100 Subject: [PATCH 13/16] Use test url for trakt notification test. fix #2798 --- couchpotato/core/notifications/trakt/main.py | 41 +++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py index c759c6db..399f76d8 100644 --- a/couchpotato/core/notifications/trakt/main.py +++ b/couchpotato/core/notifications/trakt/main.py @@ -10,6 +10,7 @@ class Trakt(Notification): 'base': 'http://api.trakt.tv/%s', 'library': 'movie/library/%s', 'unwatchlist': 'movie/unwatchlist/%s', + 'test': 'account/test/%s', } listen_to = ['movie.downloaded'] @@ -17,25 +18,39 @@ class Trakt(Notification): def notify(self, message = '', data = None, listener = None): if not data: data = {} - post_data = { - 'username': self.conf('automation_username'), - 'password' : self.conf('automation_password'), - 'movies': [{ - 'imdb_id': data['library']['identifier'], - 'title': data['library']['titles'][0]['title'], - 'year': data['library']['year'] - }] if data else [] - } + if listener == 'test': - result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data) - if self.conf('remove_watchlist_enabled'): - result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data) + post_data = { + 'username': self.conf('automation_username'), + 'password': self.conf('automation_password'), + } - return result + result = self.call((self.urls['test'] % self.conf('automation_api_key')), post_data) + + return result + + else: + + post_data = { + 'username': self.conf('automation_username'), + 'password': self.conf('automation_password'), + 'movies': [{ + 'imdb_id': data['library']['identifier'], + 'title': data['library']['titles'][0]['title'], + 'year': data['library']['year'] + }] if data else [] + } + + result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data) + if self.conf('remove_watchlist_enabled'): + result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data) + + return result def call(self, method_url, post_data): try: + response = self.getJsonData(self.urls['base'] % method_url, data = post_data, cache_timeout = 1) if response: if response.get('status') == "success": From f50c8504cf9bf47462d127574d30ac3f674a1820 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 14:19:15 +0100 Subject: [PATCH 14/16] Encode before copy metadata. fix #2832 --- couchpotato/core/plugins/base.py | 2 +- couchpotato/core/providers/metadata/base.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py index e70c3bc6..378ed50c 100644 --- a/couchpotato/core/plugins/base.py +++ b/couchpotato/core/plugins/base.py @@ -110,7 +110,7 @@ class Plugin(object): f.write(content) f.close() os.chmod(path, Env.getPermission('file')) - except Exception as e: + except: log.error('Unable writing to file "%s": %s', (path, traceback.format_exc())) if os.path.isfile(path): os.remove(path) diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py index 72d07609..d1274adf 100644 --- a/couchpotato/core/providers/metadata/base.py +++ b/couchpotato/core/providers/metadata/base.py @@ -1,4 +1,5 @@ from couchpotato.core.event import addEvent, fireEvent +from couchpotato.core.helpers.encoding import sp from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.logger import CPLog from couchpotato.core.plugins.base import Plugin @@ -48,6 +49,9 @@ class MetaDataBase(Plugin): if content: log.debug('Creating %s file: %s', (file_type, name)) if os.path.isfile(content): + content = sp(content) + name = sp(name) + shutil.copy2(content, name) shutil.copyfile(content, name) @@ -59,7 +63,7 @@ class MetaDataBase(Plugin): group['renamed_files'].append(name) try: - os.chmod(name, Env.getPermission('file')) + os.chmod(sp(name), Env.getPermission('file')) except: log.debug('Failed setting permissions for %s: %s', (name, traceback.format_exc())) From 027ff43dfd3ea291bd5cce9c943db059e2224d4f Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 14:55:35 +0100 Subject: [PATCH 15/16] Path encode files in rename. fix #2846 --- couchpotato/core/plugins/renamer/main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index f86bebad..1bf4ec91 100755 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -317,7 +317,7 @@ class Renamer(Plugin): cd = 1 if multiple else 0 for current_file in sorted(list(group['files'][file_type])): - current_file = toUnicode(current_file) + current_file = sp(current_file) # Original filename replacements['original'] = os.path.splitext(os.path.basename(current_file))[0] @@ -607,7 +607,7 @@ class Renamer(Plugin): rename_files = {} def test(s): - return current_file[:-len(replacements['ext'])] in s + return current_file[:-len(replacements['ext'])] in sp(s) for extra in set(filter(test, group['files'][extra_type])): replacements['ext'] = getExt(extra) From 8b747dff9b4612e2e19f3072533883d3a887c203 Mon Sep 17 00:00:00 2001 From: Ruud Date: Sun, 16 Feb 2014 15:48:55 +0100 Subject: [PATCH 16/16] Use correct var name in nzbvortex --- couchpotato/core/downloaders/nzbvortex/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index 205ceb1b..236e624c 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/couchpotato/core/downloaders/nzbvortex/main.py @@ -36,7 +36,7 @@ class NZBVortex(Downloader): time.sleep(10) raw_statuses = self.call('nzb') - nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(item['nzbFileName']) == nzb_filename][0] + nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0] return self.downloadReturnId(nzb_id) except: log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())