diff --git a/couchpotato/core/_base/clientscript/main.py b/couchpotato/core/_base/clientscript/main.py
index 248d2bc..c1be7e7 100644
--- a/couchpotato/core/_base/clientscript/main.py
+++ b/couchpotato/core/_base/clientscript/main.py
@@ -49,6 +49,7 @@ class ClientScript(Plugin):
'scripts/page/settings.js',
'scripts/page/about.js',
'scripts/page/manage.js',
+ 'scripts/misc/downloaders.js',
],
}
diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py
index 71da65e..3bcf1f3 100644
--- a/couchpotato/core/downloaders/base.py
+++ b/couchpotato/core/downloaders/base.py
@@ -1,4 +1,5 @@
from base64 import b32decode, b16encode
+from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
@@ -42,6 +43,7 @@ class Downloader(Provider):
addEvent('download.remove_failed', self._removeFailed)
addEvent('download.pause', self._pause)
addEvent('download.process_complete', self._processComplete)
+ addApiView('download.%s.test' % self.getName().lower(), self._test)
def getEnabledProtocol(self):
for download_protocol in self.protocol:
@@ -158,6 +160,15 @@ class Downloader(Provider):
(d_manual and manual or d_manual is False) and \
(not data or self.isCorrectProtocol(data.get('protocol')))
+ def _test(self):
+ t = self.test()
+ if isinstance(t, tuple):
+ return {'success': t[0], 'msg': t[1]}
+ return {'success': t}
+
+ def test(self):
+ return False
+
def _pause(self, release_download, pause = True):
if self.isDisabled(manual = True, data = {}):
return
diff --git a/couchpotato/core/downloaders/blackhole/main.py b/couchpotato/core/downloaders/blackhole/main.py
index 8449d09..9a01835 100644
--- a/couchpotato/core/downloaders/blackhole/main.py
+++ b/couchpotato/core/downloaders/blackhole/main.py
@@ -1,5 +1,6 @@
from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
import os
@@ -67,6 +68,20 @@ class Blackhole(Downloader):
return False
+ def test(self):
+ directory = self.conf('directory')
+ if directory and os.path.isdir(directory):
+
+ test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
+
+ # Check if folder is writable
+ self.createFile(test_file, 'This is a test file')
+ if os.path.isfile(test_file):
+ os.remove(test_file)
+ return True
+
+ return False
+
def getEnabledProtocol(self):
if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledProtocol()
diff --git a/couchpotato/core/downloaders/deluge/main.py b/couchpotato/core/downloaders/deluge/main.py
index c5f8016..5930095 100644
--- a/couchpotato/core/downloaders/deluge/main.py
+++ b/couchpotato/core/downloaders/deluge/main.py
@@ -20,14 +20,14 @@ class Deluge(Downloader):
log = CPLog(__name__)
drpc = None
- def connect(self):
+ def connect(self, reconnect = False):
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
- if not self.drpc:
+ if not self.drpc or reconnect:
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return self.drpc
@@ -86,6 +86,11 @@ class Deluge(Downloader):
log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent)
+ def test(self):
+ if self.connect(True) and self.drpc.test():
+ return True
+ return False
+
def getAllDownloadStatus(self, ids):
log.debug('Checking Deluge download status.')
@@ -178,6 +183,13 @@ class DelugeRPC(object):
self.client = DelugeClient()
self.client.connect(self.host, int(self.port), self.username, self.password)
+ def test(self):
+ try:
+ self.connect()
+ except:
+ return False
+ return True
+
def add_torrent_magnet(self, torrent, options):
torrent_id = False
try:
diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py
index a690572..3dad867 100644
--- a/couchpotato/core/downloaders/nzbget/main.py
+++ b/couchpotato/core/downloaders/nzbget/main.py
@@ -16,7 +16,6 @@ log = CPLog(__name__)
class NZBGet(Downloader):
protocol = ['nzb']
-
rpc = 'xmlrpc'
def download(self, data = None, media = None, filedata = None):
@@ -31,8 +30,7 @@ class NZBGet(Downloader):
nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
- url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
- rpc = xmlrpclib.ServerProxy(url)
+ rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
@@ -68,12 +66,31 @@ class NZBGet(Downloader):
log.error('NZBGet could not add %s to the queue.', nzb_name)
return False
+ def test(self):
+ rpc = self.getRPC()
+
+ try:
+ if rpc.writelog('INFO', 'CouchPotato connected to test connection'):
+ log.debug('Successfully connected to NZBGet')
+ else:
+ log.info('Successfully connected to NZBGet, but unable to send a message')
+ except socket.error:
+ log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
+ return False
+ except xmlrpclib.ProtocolError as e:
+ if e.errcode == 401:
+ log.error('Password is incorrect.')
+ else:
+ log.error('Protocol Error: %s', e)
+ return False
+
+ return True
+
def getAllDownloadStatus(self, ids):
log.debug('Checking NZBGet download status.')
- url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
- rpc = xmlrpclib.ServerProxy(url)
+ rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
@@ -158,8 +175,7 @@ class NZBGet(Downloader):
log.info('%s failed downloading, deleting...', release_download['name'])
- url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
- rpc = xmlrpclib.ServerProxy(url)
+ rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
@@ -194,3 +210,7 @@ class NZBGet(Downloader):
return False
return True
+
+ def getRPC(self):
+ url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
+ return xmlrpclib.ServerProxy(url)
diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py
index 205ceb1..d1525c8 100644
--- a/couchpotato/core/downloaders/nzbvortex/main.py
+++ b/couchpotato/core/downloaders/nzbvortex/main.py
@@ -36,12 +36,20 @@ 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())
return False
+ def test(self):
+ try:
+ login_result = self.login()
+ except:
+ return False
+
+ return login_result
+
def getAllDownloadStatus(self, ids):
raw_statuses = self.call('nzb')
diff --git a/couchpotato/core/downloaders/pneumatic/main.py b/couchpotato/core/downloaders/pneumatic/main.py
index 6af22d2..bc1f6d0 100644
--- a/couchpotato/core/downloaders/pneumatic/main.py
+++ b/couchpotato/core/downloaders/pneumatic/main.py
@@ -1,5 +1,6 @@
from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader
+from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog
import os
import traceback
@@ -56,3 +57,17 @@ class Pneumatic(Downloader):
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False
return False
+
+ def test(self):
+ directory = self.conf('directory')
+ if directory and os.path.isdir(directory):
+
+ test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
+
+ # Check if folder is writable
+ self.createFile(test_file, 'This is a test file')
+ if os.path.isfile(test_file):
+ os.remove(test_file)
+ return True
+
+ return False
diff --git a/couchpotato/core/downloaders/rtorrent/__init__.py b/couchpotato/core/downloaders/rtorrent/__init__.py
index 4a593fd..f793cad 100755
--- a/couchpotato/core/downloaders/rtorrent/__init__.py
+++ b/couchpotato/core/downloaders/rtorrent/__init__.py
@@ -31,8 +31,8 @@ config = [{
{
'name': 'host',
'default': 'localhost:80',
- 'description': 'Hostname with port or XML-RPC Endpoint URI. Usually scgi://localhost:5000 '
- 'or localhost:80'
+ 'description': 'RPC Communication URI. Usually scgi://localhost:5000, '
+ 'httprpc://localhost/rutorrent or localhost:80'
},
{
'name': 'ssl',
@@ -46,7 +46,7 @@ config = [{
'type': 'string',
'default': 'RPC2',
'advanced': True,
- 'description': 'Change if you don\'t run rTorrent RPC at the default url.',
+ 'description': 'Change if your RPC mount is at a different path.',
},
{
'name': 'username',
diff --git a/couchpotato/core/downloaders/rtorrent/main.py b/couchpotato/core/downloaders/rtorrent/main.py
index cfd1dce..c5850f9 100755
--- a/couchpotato/core/downloaders/rtorrent/main.py
+++ b/couchpotato/core/downloaders/rtorrent/main.py
@@ -1,14 +1,15 @@
-from base64 import b16encode, b32decode
-from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog
+from base64 import b16encode, b32decode
+from bencode import bencode, bdecode
from datetime import timedelta
from hashlib import sha1
from rtorrent import RTorrent
from rtorrent.err import MethodError
+from urlparse import urlparse
import os
log = CPLog(__name__)
@@ -18,12 +19,14 @@ class rTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet']
rt = None
+ error_msg = ''
# Migration url to host options
def __init__(self):
super(rTorrent, self).__init__()
addEvent('app.load', self.migrate)
+ addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
def migrate(self):
@@ -37,12 +40,25 @@ class rTorrent(Downloader):
self.deleteConf('url')
- def connect(self):
+ 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, reconnect = False):
# Already connected?
- if self.rt is not None:
+ if not reconnect and self.rt is not None:
return self.rt
- url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + self.conf('rpc_url')
+ url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
+ parsed = urlparse(url)
+
+ # rpc_url is only used on http/https scgi pass-through
+ if parsed.scheme in ['http', 'https']:
+ url += self.conf('rpc_url')
if self.conf('username') and self.conf('password'):
self.rt = RTorrent(
@@ -53,8 +69,24 @@ class rTorrent(Downloader):
else:
self.rt = RTorrent(url)
+ self.error_msg = ''
+ try:
+ self.rt._verify_conn()
+ except AssertionError as e:
+ self.error_msg = e.message
+ self.rt = None
+
return self.rt
+ def test(self):
+ if self.connect(True):
+ return True
+
+ if self.error_msg:
+ return False, 'Connection failed: ' + self.error_msg
+
+ return False
+
def _update_provider_group(self, name, data):
if data.get('seed_time'):
log.info('seeding time ignored, not supported')
@@ -104,7 +136,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 = {}
@@ -159,6 +191,21 @@ class rTorrent(Downloader):
log.error('Failed to send torrent to rTorrent: %s', err)
return False
+ def getTorrentStatus(self, torrent):
+ if torrent.hashing or torrent.hash_checking or torrent.message:
+ return 'busy'
+
+ if not torrent.complete:
+ return 'busy'
+
+ if not torrent.open:
+ return 'completed'
+
+ if torrent.state and torrent.active:
+ return 'seeding'
+
+ return 'busy'
+
def getAllDownloadStatus(self, ids):
log.debug('Checking rTorrent download status.')
@@ -183,17 +230,10 @@ class rTorrent(Downloader):
torrent_files.append(sp(file_path))
- status = 'busy'
- if torrent.complete:
- if torrent.active:
- status = 'seeding'
- else:
- status = 'completed'
-
release_downloads.append({
'id': torrent.info_hash,
'name': torrent.name,
- 'status': status,
+ 'status': self.getTorrentStatus(torrent),
'seed_ratio': torrent.ratio,
'original_status': torrent.state,
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py
index 72c2370..ba58c09 100644
--- a/couchpotato/core/downloaders/sabnzbd/main.py
+++ b/couchpotato/core/downloaders/sabnzbd/main.py
@@ -64,6 +64,26 @@ class Sabnzbd(Downloader):
log.error('Error getting data from SABNZBd: %s', sab_data)
return False
+ def test(self):
+ try:
+ sab_data = self.call({
+ 'mode': 'version',
+ })
+ v = sab_data.split('.')
+ if int(v[0]) == 0 and int(v[1]) < 7:
+ return False, 'Your Sabnzbd client is too old, please update to newest version.'
+
+ # the version check will work even with wrong api key, so we need the next check as well
+ sab_data = self.call({
+ 'mode': 'qstatus',
+ })
+ if not sab_data:
+ return False
+ except:
+ return False
+
+ return True
+
def getAllDownloadStatus(self, ids):
log.debug('Checking SABnzbd download status.')
diff --git a/couchpotato/core/downloaders/synology/main.py b/couchpotato/core/downloaders/synology/main.py
index f964f37..7e5b609 100644
--- a/couchpotato/core/downloaders/synology/main.py
+++ b/couchpotato/core/downloaders/synology/main.py
@@ -45,6 +45,16 @@ class Synology(Downloader):
finally:
return self.downloadReturnId('') if response else False
+ def test(self):
+ host = cleanHost(self.conf('host'), protocol = False).split(':')
+ try:
+ srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
+ test_result = srpc.test()
+ except:
+ return False
+
+ return test_result
+
def getEnabledProtocol(self):
if self.conf('use_for') == 'both':
return super(Synology, self).getEnabledProtocol()
@@ -147,3 +157,6 @@ class SynologyRPC(object):
self._logout()
return result
+
+ def test(self):
+ return bool(self._login())
diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py
index 2daeab4..4c42bf0 100644
--- a/couchpotato/core/downloaders/transmission/main.py
+++ b/couchpotato/core/downloaders/transmission/main.py
@@ -19,14 +19,14 @@ class Transmission(Downloader):
log = CPLog(__name__)
trpc = None
- def connect(self):
+ def connect(self, reconnect = False):
# Load host from config and split out port.
host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.')
return False
- if not self.trpc:
+ if not self.trpc or reconnect:
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
return self.trpc
@@ -83,6 +83,11 @@ class Transmission(Downloader):
log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
+ def test(self):
+ if self.connect(True) and self.trpc.get_session():
+ return True
+ return False
+
def getAllDownloadStatus(self, ids):
log.debug('Checking Transmission download status.')
diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py
index e0d6a92..6a5e425 100644
--- a/couchpotato/core/downloaders/utorrent/main.py
+++ b/couchpotato/core/downloaders/utorrent/main.py
@@ -115,6 +115,17 @@ class uTorrent(Downloader):
return self.downloadReturnId(torrent_hash)
+ def test(self):
+ if self.connect():
+ build_version = self.utorrent_api.get_build()
+ if not build_version:
+ return False
+ if build_version < 25406: # This build corresponds to version 3.0.0 stable
+ return False, 'Your uTorrent client is too old, please update to newest version.'
+ return True
+
+ return False
+
def getAllDownloadStatus(self, ids):
log.debug('Checking uTorrent download status.')
@@ -322,3 +333,10 @@ class uTorrentAPI(object):
def get_files(self, hash):
action = 'action=getfiles&hash=%s' % hash
return self._request(action)
+
+ def get_build(self):
+ data = self._request('')
+ if not data:
+ return False
+ response = json.loads(data)
+ return int(response.get('build'))
diff --git a/couchpotato/core/notifications/pushbullet/main.py b/couchpotato/core/notifications/pushbullet/main.py
index 15120f0..487fb3a 100644
--- a/couchpotato/core/notifications/pushbullet/main.py
+++ b/couchpotato/core/notifications/pushbullet/main.py
@@ -1,5 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode
-from couchpotato.core.helpers.variable import tryInt
+from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import base64
@@ -32,7 +32,7 @@ class Pushbullet(Notification):
response = self.request(
'pushes',
cache = False,
- device_id = device,
+ device_iden = device,
type = 'note',
title = self.default_title,
body = toUnicode(message)
@@ -46,24 +46,7 @@ class Pushbullet(Notification):
return successful == len(devices)
def getDevices(self):
- devices = [d.strip() for d in self.conf('devices').split(',')]
-
- # Remove empty items
- devices = [d for d in devices if len(d)]
-
- # Break on any ids that aren't integers
- valid_devices = []
-
- for device_id in devices:
- d = tryInt(device_id, None)
-
- if not d:
- log.error('Device ID "%s" is not valid', device_id)
- return None
-
- valid_devices.append(d)
-
- return valid_devices
+ return splitString(self.conf('devices'))
def request(self, method, cache = True, **kwargs):
try:
diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py
index c759c6d..399f76d 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":
diff --git a/couchpotato/core/plugins/base.py b/couchpotato/core/plugins/base.py
index 0625535..378ed50 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)
@@ -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):
diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py
index f86beba..1bf4ec9 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)
diff --git a/couchpotato/core/providers/automation/goodfilms/main.py b/couchpotato/core/providers/automation/goodfilms/main.py
index c4a7bd9..e668a4f 100644
--- a/couchpotato/core/providers/automation/goodfilms/main.py
+++ b/couchpotato/core/providers/automation/goodfilms/main.py
@@ -7,7 +7,7 @@ log = CPLog(__name__)
class Goodfilms(Automation):
- url = 'http://goodfil.ms/%s/queue?page=%d&without_layout=1'
+ url = 'https://goodfil.ms/%s/queue?page=%d&without_layout=1'
interval = 1800
diff --git a/couchpotato/core/providers/automation/itunes/main.py b/couchpotato/core/providers/automation/itunes/main.py
index 7676324..086c981 100644
--- a/couchpotato/core/providers/automation/itunes/main.py
+++ b/couchpotato/core/providers/automation/itunes/main.py
@@ -22,7 +22,7 @@ class ITunes(Automation, RSS):
urls = splitString(self.conf('automation_urls'))
namespace = 'http://www.w3.org/2005/Atom'
- namespace_im = 'http://itunes.apple.com/rss'
+ namespace_im = 'https://rss.itunes.apple.com'
index = -1
for url in urls:
diff --git a/couchpotato/core/providers/metadata/base.py b/couchpotato/core/providers/metadata/base.py
index 72d0760..d1274ad 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()))
diff --git a/couchpotato/core/providers/nzb/nzbclub/main.py b/couchpotato/core/providers/nzb/nzbclub/main.py
index 778cdbc..643f247 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': 'https://www.nzbclub.com/nzbfeeds.aspx?%s',
}
http_time_between_calls = 4 #seconds
diff --git a/couchpotato/core/providers/torrent/ilovetorrents/main.py b/couchpotato/core/providers/torrent/ilovetorrents/main.py
index 6d56ea4..f8ed67a 100644
--- a/couchpotato/core/providers/torrent/ilovetorrents/main.py
+++ b/couchpotato/core/providers/torrent/ilovetorrents/main.py
@@ -12,12 +12,12 @@ log = CPLog(__name__)
class ILoveTorrents(TorrentProvider):
urls = {
- 'download': 'http://www.ilovetorrents.me/%s',
- 'detail': 'http://www.ilovetorrents.me/%s',
- 'search': 'http://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
- 'test': 'http://www.ilovetorrents.me/',
- 'login': 'http://www.ilovetorrents.me/takelogin.php',
- 'login_check': 'http://www.ilovetorrents.me'
+ 'download': 'https://www.ilovetorrents.me/%s',
+ 'detail': 'https//www.ilovetorrents.me/%s',
+ 'search': 'https://www.ilovetorrents.me/browse.php?search=%s&page=%s&cat=%s',
+ 'test': 'https://www.ilovetorrents.me/',
+ 'login': 'https://www.ilovetorrents.me/takelogin.php',
+ 'login_check': 'https://www.ilovetorrents.me'
}
cat_ids = [
diff --git a/couchpotato/core/providers/torrent/iptorrents/main.py b/couchpotato/core/providers/torrent/iptorrents/main.py
index 35ce25c..4a2c6db 100644
--- a/couchpotato/core/providers/torrent/iptorrents/main.py
+++ b/couchpotato/core/providers/torrent/iptorrents/main.py
@@ -11,11 +11,11 @@ log = CPLog(__name__)
class IPTorrents(TorrentProvider):
urls = {
- 'test': 'http://www.iptorrents.com/',
- 'base_url': 'http://www.iptorrents.com',
- 'login': 'http://www.iptorrents.com/torrents/',
- 'login_check': 'http://www.iptorrents.com/inbox.php',
- 'search': 'http://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d',
+ 'test': 'https://www.iptorrents.com/',
+ 'base_url': 'https://www.iptorrents.com',
+ 'login': 'https://www.iptorrents.com/torrents/',
+ 'login_check': 'https://www.iptorrents.com/inbox.php',
+ 'search': 'https://www.iptorrents.com/torrents/?l%d=1%s&q=%s&qf=ti&p=%d',
}
cat_ids = [
diff --git a/couchpotato/core/providers/torrent/passthepopcorn/main.py b/couchpotato/core/providers/torrent/passthepopcorn/main.py
index 66cad33..57a36c2 100644
--- a/couchpotato/core/providers/torrent/passthepopcorn/main.py
+++ b/couchpotato/core/providers/torrent/passthepopcorn/main.py
@@ -89,11 +89,11 @@ class PassThePopcorn(TorrentProvider):
if 'GoldenPopcorn' in torrent and torrent['GoldenPopcorn']:
torrentdesc += ' HQ'
if self.conf('prefer_golden'):
- torrentscore += 200
+ torrentscore += 5000
if 'Scene' in torrent and torrent['Scene']:
torrentdesc += ' Scene'
if self.conf('prefer_scene'):
- torrentscore += 50
+ torrentscore += 2000
if 'RemasterTitle' in torrent and torrent['RemasterTitle']:
torrentdesc += self.htmlToASCII(' %s' % torrent['RemasterTitle'])
diff --git a/couchpotato/core/providers/torrent/sceneaccess/main.py b/couchpotato/core/providers/torrent/sceneaccess/main.py
index a43c49b..c1c871e 100644
--- a/couchpotato/core/providers/torrent/sceneaccess/main.py
+++ b/couchpotato/core/providers/torrent/sceneaccess/main.py
@@ -15,7 +15,7 @@ class SceneAccess(TorrentProvider):
'login': 'https://www.sceneaccess.eu/login',
'login_check': 'https://www.sceneaccess.eu/inbox',
'detail': 'https://www.sceneaccess.eu/details?id=%s',
- 'search': 'https://www.sceneaccess.eu/browse?method=2&c%d=%d',
+ 'search': 'https://www.sceneaccess.eu/browse?c%d=%d',
'download': 'https://www.sceneaccess.eu/%s',
}
@@ -40,7 +40,7 @@ class SceneAccess(TorrentProvider):
arguments = tryUrlencode({
'search': movie['library']['identifier'],
- 'method': 1,
+ 'method': 3,
})
url = "%s&%s" % (url, arguments)
diff --git a/couchpotato/core/providers/torrent/thepiratebay/main.py b/couchpotato/core/providers/torrent/thepiratebay/main.py
index f8b7778..c366c51 100644
--- a/couchpotato/core/providers/torrent/thepiratebay/main.py
+++ b/couchpotato/core/providers/torrent/thepiratebay/main.py
@@ -31,15 +31,13 @@ class ThePirateBay(TorrentMagnetProvider):
proxy_list = [
'https://tpb.ipredator.se',
'https://thepiratebay.se',
- 'https://depiraatbaai.be',
- 'https://piratereverse.info',
- 'https://tpb.pirateparty.org.uk',
- 'https://argumentomteemigreren.nl',
- 'https://livepirate.com',
+ 'http://pirateproxy.ca',
+ 'http://tpb.al',
+ 'http://www.tpb.gr',
+ 'http://nl.tpb.li',
+ 'http://proxybay.eu',
'https://www.getpirate.com',
- 'https://tpb.partipirate.org',
- 'https://tpb.piraten.lu',
- 'https://kuiken.co',
+ 'http://pirateproxy.ca',
]
def _searchOnTitle(self, title, movie, quality, results):
diff --git a/couchpotato/core/providers/torrent/torrentshack/__init__.py b/couchpotato/core/providers/torrent/torrentshack/__init__.py
index 0e55211..058236e 100644
--- a/couchpotato/core/providers/torrent/torrentshack/__init__.py
+++ b/couchpotato/core/providers/torrent/torrentshack/__init__.py
@@ -11,7 +11,7 @@ config = [{
'tab': 'searcher',
'list': 'torrent_providers',
'name': 'TorrentShack',
- 'description': 'See TorrentShack',
+ 'description': 'See TorrentShack',
'options': [
{
'name': 'enabled',
diff --git a/couchpotato/core/providers/torrent/yify/main.py b/couchpotato/core/providers/torrent/yify/main.py
index 6deaf7b..fe1b820 100644
--- a/couchpotato/core/providers/torrent/yify/main.py
+++ b/couchpotato/core/providers/torrent/yify/main.py
@@ -18,9 +18,9 @@ class Yify(TorrentMagnetProvider):
proxy_list = [
'http://yify.unlocktorrent.com',
- 'http://yify.ftwnet.co.uk',
'http://yify-torrents.com.come.in',
'http://yts.re',
+ 'http://yts.im'
'https://yify-torrents.im',
]
diff --git a/couchpotato/core/settings/__init__.py b/couchpotato/core/settings/__init__.py
index 3b57517..0e65c77 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,
diff --git a/couchpotato/static/scripts/misc/downloaders.js b/couchpotato/static/scripts/misc/downloaders.js
new file mode 100644
index 0000000..5127275
--- /dev/null
+++ b/couchpotato/static/scripts/misc/downloaders.js
@@ -0,0 +1,75 @@
+var DownloadersBase = new Class({
+
+ Implements: [Events],
+
+ initialize: function(){
+ var self = this;
+
+ // Add test buttons to settings page
+ App.addEvent('load', self.addTestButtons.bind(self));
+
+ },
+
+ // Downloaders setting tests
+ addTestButtons: function(){
+ var self = this;
+
+ var setting_page = App.getPage('Settings');
+ setting_page.addEvent('create', function(){
+ Object.each(setting_page.tabs.downloaders.groups, self.addTestButton.bind(self))
+ })
+
+ },
+
+ addTestButton: function(fieldset, plugin_name){
+ var self = this,
+ button_name = self.testButtonName(fieldset);
+
+ if(button_name.contains('Downloaders')) return;
+
+ new Element('.ctrlHolder.test_button').adopt(
+ new Element('a.button', {
+ 'text': button_name,
+ 'events': {
+ 'click': function(){
+ var button = fieldset.getElement('.test_button .button');
+ button.set('text', 'Connecting...');
+
+ Api.request('download.'+plugin_name+'.test', {
+ 'onComplete': function(json){
+
+ button.set('text', button_name);
+
+ if(json.success){
+ var message = new Element('span.success', {
+ 'text': 'Connection successful'
+ }).inject(button, 'after')
+ }
+ else {
+ var msg_text = 'Connection failed. Check logs for details.';
+ if(json.hasOwnProperty('msg')) msg_text = json.msg;
+ var message = new Element('span.failed', {
+ 'text': msg_text
+ }).inject(button, 'after')
+ }
+
+ (function(){
+ message.destroy();
+ }).delay(3000)
+ }
+ });
+ }
+ }
+ })
+ ).inject(fieldset);
+
+ },
+
+ testButtonName: function(fieldset){
+ var name = String(fieldset.getElement('h2').innerHTML).substring(0,String(fieldset.getElement('h2').innerHTML).indexOf("= MIN_RTORRENT_VERSION
diff --git a/libs/rtorrent/common.py b/libs/rtorrent/common.py
index 371c71c..668865b 100755
--- a/libs/rtorrent/common.py
+++ b/libs/rtorrent/common.py
@@ -17,7 +17,8 @@
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-
+import urlparse
+import os
from rtorrent.compat import is_py3
@@ -84,3 +85,67 @@ def safe_repr(fmt, *args, **kwargs):
return out.encode("utf-8")
else:
return fmt.format(*args, **kwargs)
+
+
+def split_path(path):
+ fragments = path.split('/')
+
+ if len(fragments) == 1:
+ return fragments
+
+ if not fragments[-1]:
+ return fragments[:-1]
+
+ return fragments
+
+
+def join_path(base, path):
+ # Return if we have a new absolute path
+ if os.path.isabs(path):
+ return path
+
+ # non-absolute base encountered
+ if base and not os.path.isabs(base):
+ raise NotImplementedError()
+
+ return '/'.join(split_path(base) + split_path(path))
+
+
+def join_uri(base, uri, construct=True):
+ p_uri = urlparse.urlparse(uri)
+
+ # Return if there is nothing to join
+ if not p_uri.path:
+ return base
+
+ scheme, netloc, path, params, query, fragment = urlparse.urlparse(base)
+
+ # Switch to 'uri' parts
+ _, _, _, params, query, fragment = p_uri
+
+ path = join_path(path, p_uri.path)
+
+ result = urlparse.ParseResult(scheme, netloc, path, params, query, fragment)
+
+ if not construct:
+ return result
+
+ # Construct from parts
+ return urlparse.urlunparse(result)
+
+
+def update_uri(uri, construct=True, **kwargs):
+ if isinstance(uri, urlparse.ParseResult):
+ uri = dict(uri._asdict())
+
+ if type(uri) is not dict:
+ raise ValueError("Unknown URI type")
+
+ uri.update(kwargs)
+
+ result = urlparse.ParseResult(**uri)
+
+ if not construct:
+ return result
+
+ return urlparse.urlunparse(result)
diff --git a/libs/tmdb3/__init__.py b/libs/tmdb3/__init__.py
index 92ca551..d5e35b3 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 3b10677..463d7a2 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 99ad4cd..1101955 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 ):
- __metaclass__ = CacheEngineType
+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 5918071..4e96581 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 a59741c..8c360da 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 97efec7..0ef0310 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 6cb874c..ebcb9d2 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 109630d..2d51dcd 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 b5cb0a9..1c8fabd 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))
-
-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')
+ 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')
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')
-
-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')
+ .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')
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 Language(NameRepr, Element):
+ code = Datapoint('iso_639_1')
+ name = Datapoint('name')
+
-class Movie( Element ):
+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')
-
- 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')
-
- adult = Datapoint('adult')
- collection = Datapoint('belongs_to_collection', handler=lambda x: \
+ 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)
+
+ popularity = Datapoint('popularity')
+ userrating = Datapoint('vote_average')
+ votes = Datapoint('vote_count')
+
+ 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 8583b99..b447b5a 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 35e0364..f85fbcf 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 ):
- pass
-class TMDBLocaleError( TMDBError ):
+class TMDBOffline(TMDBError):
pass
+
+class TMDBLocaleError(TMDBError):
+ pass
diff --git a/libs/tmdb3/util.py b/libs/tmdb3/util.py
index bba9fcc..a0d2e28 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'
-