Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -49,6 +49,7 @@ class ClientScript(Plugin):
|
||||
'scripts/page/settings.js',
|
||||
'scripts/page/about.js',
|
||||
'scripts/page/manage.js',
|
||||
'scripts/misc/downloaders.js',
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,8 +31,8 @@ config = [{
|
||||
{
|
||||
'name': 'host',
|
||||
'default': 'localhost:80',
|
||||
'description': 'Hostname with port or XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
|
||||
'or <strong>localhost:80</strong>'
|
||||
'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
|
||||
'<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
|
||||
},
|
||||
{
|
||||
'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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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.')
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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'])
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -11,7 +11,7 @@ config = [{
|
||||
'tab': 'searcher',
|
||||
'list': 'torrent_providers',
|
||||
'name': 'TorrentShack',
|
||||
'description': 'See <a href="http://www.torrentshack.net/">TorrentShack</a>',
|
||||
'description': 'See <a href="https://www.torrentshack.net/">TorrentShack</a>',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
75
couchpotato/static/scripts/misc/downloaders.js
Normal file
75
couchpotato/static/scripts/misc/downloaders.js
Normal file
@@ -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("<span"));
|
||||
return 'Test '+name;
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
window.Downloaders = new DownloadersBase();
|
||||
@@ -22,8 +22,8 @@ import os.path
|
||||
import time
|
||||
import xmlrpclib
|
||||
|
||||
from rtorrent.common import find_torrent, \
|
||||
is_valid_port, convert_version_tuple_to_str
|
||||
from rtorrent.common import find_torrent, join_uri, \
|
||||
update_uri, is_valid_port, convert_version_tuple_to_str
|
||||
from rtorrent.lib.torrentparser import TorrentParser
|
||||
from rtorrent.lib.xmlrpc.http import HTTPServerProxy
|
||||
from rtorrent.lib.xmlrpc.scgi import SCGIServerProxy
|
||||
@@ -48,18 +48,18 @@ class RTorrent:
|
||||
|
||||
def __init__(self, uri, username=None, password=None,
|
||||
verify=False, sp=None, sp_kwargs=None):
|
||||
self.uri = uri # : From X{__init__(self, url)}
|
||||
self.uri = self._transform_uri(uri) # : From X{__init__(self, url)}
|
||||
|
||||
self.username = username
|
||||
self.password = password
|
||||
|
||||
self.schema = urllib.splittype(uri)[0]
|
||||
self.scheme = urllib.splittype(self.uri)[0]
|
||||
|
||||
if sp:
|
||||
self.sp = sp
|
||||
elif self.schema in ['http', 'https']:
|
||||
elif self.scheme in ['http', 'https']:
|
||||
self.sp = HTTPServerProxy
|
||||
elif self.schema == 'scgi':
|
||||
elif self.scheme == 'scgi':
|
||||
self.sp = SCGIServerProxy
|
||||
else:
|
||||
raise NotImplementedError()
|
||||
@@ -74,10 +74,23 @@ class RTorrent:
|
||||
if verify is True:
|
||||
self._verify_conn()
|
||||
|
||||
def _transform_uri(self, uri):
|
||||
scheme = urllib.splittype(uri)[0]
|
||||
|
||||
if scheme == 'httprpc' or scheme.startswith('httprpc+'):
|
||||
# Try find HTTPRPC transport (token after '+' in 'httprpc+https'), otherwise assume HTTP
|
||||
transport = scheme[scheme.index('+') + 1:] if '+' in scheme else 'http'
|
||||
|
||||
# Transform URI with new path and scheme
|
||||
uri = join_uri(uri, 'plugins/httprpc/action.php', construct=False)
|
||||
return update_uri(uri, scheme=transport)
|
||||
|
||||
return uri
|
||||
|
||||
def _get_conn(self):
|
||||
"""Get ServerProxy instance"""
|
||||
if self.username is not None and self.password is not None:
|
||||
if self.schema == 'scgi':
|
||||
if self.scheme == 'scgi':
|
||||
raise NotImplementedError()
|
||||
|
||||
return self.sp(
|
||||
@@ -98,6 +111,13 @@ class RTorrent:
|
||||
"Error: Minimum rTorrent version required is {0}".format(
|
||||
MIN_RTORRENT_VERSION_STR)
|
||||
|
||||
def test_connection(self):
|
||||
try:
|
||||
self._verify_conn()
|
||||
except:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _meets_version_requirement(self):
|
||||
return self._get_client_version_tuple() >= MIN_RTORRENT_VERSION
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -10,35 +10,46 @@
|
||||
import time
|
||||
from weakref import ref
|
||||
|
||||
class Engines( object ):
|
||||
|
||||
class Engines(object):
|
||||
"""
|
||||
Static collector for engines to register against.
|
||||
"""
|
||||
def __init__(self):
|
||||
self._engines = {}
|
||||
|
||||
def register(self, engine):
|
||||
self._engines[engine.__name__] = engine
|
||||
self._engines[engine.name] = engine
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self._engines[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
return self._engines.__contains__(key)
|
||||
|
||||
Engines = Engines()
|
||||
|
||||
class CacheEngineType( type ):
|
||||
|
||||
class CacheEngineType(type):
|
||||
"""
|
||||
Cache Engine Metaclass that registers new engines against the cache
|
||||
for named selection and use.
|
||||
"""
|
||||
def __init__(mcs, name, bases, attrs):
|
||||
super(CacheEngineType, mcs).__init__(name, bases, attrs)
|
||||
def __init__(cls, name, bases, attrs):
|
||||
super(CacheEngineType, cls).__init__(name, bases, attrs)
|
||||
if name != 'CacheEngine':
|
||||
# skip base class
|
||||
Engines.register(mcs)
|
||||
Engines.register(cls)
|
||||
|
||||
class CacheEngine( object ):
|
||||
|
||||
class CacheEngine(object):
|
||||
__metaclass__ = CacheEngineType
|
||||
|
||||
name = 'unspecified'
|
||||
|
||||
def __init__(self, parent):
|
||||
self.parent = ref(parent)
|
||||
|
||||
def configure(self):
|
||||
raise RuntimeError
|
||||
def get(self, date):
|
||||
@@ -48,7 +59,8 @@ class CacheEngine( object ):
|
||||
def expire(self, key):
|
||||
raise RuntimeError
|
||||
|
||||
class CacheObject( object ):
|
||||
|
||||
class CacheObject(object):
|
||||
"""
|
||||
Cache object class, containing one stored record.
|
||||
"""
|
||||
@@ -64,7 +76,7 @@ class CacheObject( object ):
|
||||
|
||||
@property
|
||||
def expired(self):
|
||||
return (self.remaining == 0)
|
||||
return self.remaining == 0
|
||||
|
||||
@property
|
||||
def remaining(self):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"<Language '{0.englishname}' ({0.ISO639_1})>".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"<Country '{0.name}' ({0.alpha2})>".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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
# (http://creativecommons.org/licenses/GPL/2.0/)
|
||||
#-----------------------
|
||||
|
||||
__title__ = "tmdb_api - Simple-to-use Python interface to TMDB's API v3 "+\
|
||||
"(www.themoviedb.org)"
|
||||
__title__ = ("tmdb_api - Simple-to-use Python interface to TMDB's API v3 " +
|
||||
"(www.themoviedb.org)")
|
||||
__author__ = "Raymond Wagner"
|
||||
__purpose__ = """
|
||||
This Python library is intended to provide a series of classes and methods
|
||||
@@ -22,7 +22,7 @@ for search and retrieval of text metadata and image URLs from TMDB.
|
||||
Preliminary API specifications can be found at
|
||||
http://help.themoviedb.org/kb/api/about-3"""
|
||||
|
||||
__version__="v0.6.17"
|
||||
__version__ = "v0.7.0"
|
||||
# 0.1.0 Initial development
|
||||
# 0.2.0 Add caching mechanism for API queries
|
||||
# 0.2.1 Temporary work around for broken search paging
|
||||
@@ -59,8 +59,9 @@ __version__="v0.6.17"
|
||||
# 0.6.14 Add support for Lists
|
||||
# 0.6.15 Add ability to search Collections
|
||||
# 0.6.16 Make absent primary images return None (previously u'')
|
||||
# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove
|
||||
# 0.6.17 Add userrating/votes to Image, add overview to Collection, remove
|
||||
# releasedate sorting from Collection Movies
|
||||
# 0.7.0 Add support for television series data
|
||||
|
||||
from request import set_key, Request
|
||||
from util import Datapoint, Datalist, Datadict, Element, NameRepr, SearchRepr
|
||||
@@ -69,10 +70,14 @@ from locales import get_locale, set_locale
|
||||
from tmdb_auth import get_session, set_session
|
||||
from tmdb_exceptions import *
|
||||
|
||||
import json
|
||||
import urllib
|
||||
import urllib2
|
||||
import datetime
|
||||
|
||||
DEBUG = False
|
||||
|
||||
|
||||
def process_date(datestr):
|
||||
try:
|
||||
return datetime.date(*[int(x) for x in datestr.split('-')])
|
||||
@@ -82,34 +87,40 @@ def process_date(datestr):
|
||||
import traceback
|
||||
_,_,tb = sys.exc_info()
|
||||
f,l,_,_ = traceback.extract_tb(tb)[-1]
|
||||
warnings.warn_explicit(('"{0}" is not a supported date format. '
|
||||
'Please fix upstream data at http://www.themoviedb.org.')\
|
||||
.format(datestr), Warning, f, l)
|
||||
warnings.warn_explicit(('"{0}" is not a supported date format. ' +
|
||||
'Please fix upstream data at ' +
|
||||
'http://www.themoviedb.org.'
|
||||
).format(datestr), Warning, f, l)
|
||||
return None
|
||||
|
||||
class Configuration( Element ):
|
||||
|
||||
class Configuration(Element):
|
||||
images = Datapoint('images')
|
||||
|
||||
def _populate(self):
|
||||
return Request('configuration')
|
||||
|
||||
Configuration = Configuration()
|
||||
|
||||
class Account( NameRepr, Element ):
|
||||
|
||||
class Account(NameRepr, Element):
|
||||
def _populate(self):
|
||||
return Request('account', session_id=self._session.sessionid)
|
||||
|
||||
id = Datapoint('id')
|
||||
adult = Datapoint('include_adult')
|
||||
country = Datapoint('iso_3166_1')
|
||||
language = Datapoint('iso_639_1')
|
||||
name = Datapoint('name')
|
||||
username = Datapoint('username')
|
||||
id = Datapoint('id')
|
||||
adult = Datapoint('include_adult')
|
||||
country = Datapoint('iso_3166_1')
|
||||
language = Datapoint('iso_639_1')
|
||||
name = Datapoint('name')
|
||||
username = Datapoint('username')
|
||||
|
||||
@property
|
||||
def locale(self):
|
||||
return get_locale(self.language, self.country)
|
||||
|
||||
|
||||
def searchMovie(query, locale=None, adult=False, year=None):
|
||||
kwargs = {'query':query, 'include_adult':adult}
|
||||
kwargs = {'query': query, 'include_adult': adult}
|
||||
if year is not None:
|
||||
try:
|
||||
kwargs['year'] = year.year
|
||||
@@ -117,6 +128,7 @@ def searchMovie(query, locale=None, adult=False, year=None):
|
||||
kwargs['year'] = year
|
||||
return MovieSearchResult(Request('search/movie', **kwargs), locale=locale)
|
||||
|
||||
|
||||
def searchMovieWithYear(query, locale=None, adult=False):
|
||||
year = None
|
||||
if (len(query) > 6) and (query[-1] == ')') and (query[-6] == '('):
|
||||
@@ -134,70 +146,95 @@ def searchMovieWithYear(query, locale=None, adult=False):
|
||||
year = None
|
||||
return searchMovie(query, locale, adult, year)
|
||||
|
||||
class MovieSearchResult( SearchRepr, PagedRequest ):
|
||||
|
||||
class MovieSearchResult(SearchRepr, PagedRequest):
|
||||
"""Stores a list of search matches."""
|
||||
_name = None
|
||||
def __init__(self, request, locale=None):
|
||||
if locale is None:
|
||||
locale = get_locale()
|
||||
super(MovieSearchResult, self).__init__(
|
||||
request.new(language=locale.language),
|
||||
lambda x: Movie(raw=x, locale=locale))
|
||||
request.new(language=locale.language),
|
||||
lambda x: Movie(raw=x, locale=locale))
|
||||
|
||||
def searchSeries(query, first_air_date_year=None, search_type=None, locale=None):
|
||||
return SeriesSearchResult(
|
||||
Request('search/tv', query=query, first_air_date_year=first_air_date_year, search_type=search_type),
|
||||
locale=locale)
|
||||
|
||||
|
||||
class SeriesSearchResult(SearchRepr, PagedRequest):
|
||||
"""Stores a list of search matches."""
|
||||
_name = None
|
||||
def __init__(self, request, locale=None):
|
||||
if locale is None:
|
||||
locale = get_locale()
|
||||
super(SeriesSearchResult, self).__init__(
|
||||
request.new(language=locale.language),
|
||||
lambda x: Series(raw=x, locale=locale))
|
||||
|
||||
def searchPerson(query, adult=False):
|
||||
return PeopleSearchResult(Request('search/person', query=query,
|
||||
include_adult=adult))
|
||||
|
||||
class PeopleSearchResult( SearchRepr, PagedRequest ):
|
||||
|
||||
class PeopleSearchResult(SearchRepr, PagedRequest):
|
||||
"""Stores a list of search matches."""
|
||||
_name = None
|
||||
def __init__(self, request):
|
||||
super(PeopleSearchResult, self).__init__(request,
|
||||
lambda x: Person(raw=x))
|
||||
super(PeopleSearchResult, self).__init__(
|
||||
request, lambda x: Person(raw=x))
|
||||
|
||||
|
||||
def searchStudio(query):
|
||||
return StudioSearchResult(Request('search/company', query=query))
|
||||
|
||||
class StudioSearchResult( SearchRepr, PagedRequest ):
|
||||
|
||||
class StudioSearchResult(SearchRepr, PagedRequest):
|
||||
"""Stores a list of search matches."""
|
||||
_name = None
|
||||
def __init__(self, request):
|
||||
super(StudioSearchResult, self).__init__(request,
|
||||
lambda x: Studio(raw=x))
|
||||
super(StudioSearchResult, self).__init__(
|
||||
request, lambda x: Studio(raw=x))
|
||||
|
||||
|
||||
def searchList(query, adult=False):
|
||||
ListSearchResult(Request('search/list', query=query, include_adult=adult))
|
||||
|
||||
class ListSearchResult( SearchRepr, PagedRequest ):
|
||||
|
||||
class ListSearchResult(SearchRepr, PagedRequest):
|
||||
"""Stores a list of search matches."""
|
||||
_name = None
|
||||
def __init__(self, request):
|
||||
super(ListSearchResult, self).__init__(request,
|
||||
lambda x: List(raw=x))
|
||||
super(ListSearchResult, self).__init__(
|
||||
request, lambda x: List(raw=x))
|
||||
|
||||
|
||||
def searchCollection(query, locale=None):
|
||||
return CollectionSearchResult(Request('search/collection', query=query),
|
||||
locale=locale)
|
||||
|
||||
class CollectionSearchResult( SearchRepr, PagedRequest ):
|
||||
|
||||
class CollectionSearchResult(SearchRepr, PagedRequest):
|
||||
"""Stores a list of search matches."""
|
||||
_name=None
|
||||
def __init__(self, request, locale=None):
|
||||
if locale is None:
|
||||
locale = get_locale()
|
||||
super(CollectionSearchResult, self).__init__(
|
||||
request.new(language=locale.language),
|
||||
lambda x: Collection(raw=x, locale=locale))
|
||||
request.new(language=locale.language),
|
||||
lambda x: Collection(raw=x, locale=locale))
|
||||
|
||||
class Image( Element ):
|
||||
filename = Datapoint('file_path', initarg=1,
|
||||
handler=lambda x: x.lstrip('/'))
|
||||
aspectratio = Datapoint('aspect_ratio')
|
||||
height = Datapoint('height')
|
||||
width = Datapoint('width')
|
||||
language = Datapoint('iso_639_1')
|
||||
userrating = Datapoint('vote_average')
|
||||
votes = Datapoint('vote_count')
|
||||
|
||||
class Image(Element):
|
||||
filename = Datapoint('file_path', initarg=1,
|
||||
handler=lambda x: x.lstrip('/'))
|
||||
aspectratio = Datapoint('aspect_ratio')
|
||||
height = Datapoint('height')
|
||||
width = Datapoint('width')
|
||||
language = Datapoint('iso_639_1')
|
||||
userrating = Datapoint('vote_average')
|
||||
votes = Datapoint('vote_count')
|
||||
|
||||
def sizes(self):
|
||||
return ['original']
|
||||
@@ -205,19 +242,28 @@ class Image( Element ):
|
||||
def geturl(self, size='original'):
|
||||
if size not in self.sizes():
|
||||
raise TMDBImageSizeError
|
||||
url = Configuration.images['base_url'].rstrip('/')
|
||||
url = Configuration.images['secure_base_url'].rstrip('/')
|
||||
return url+'/{0}/{1}'.format(size, self.filename)
|
||||
|
||||
# sort preferring locale's language, but keep remaining ordering consistent
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Image):
|
||||
return False
|
||||
return (self.language == self._locale.language) \
|
||||
and (self.language != other.language)
|
||||
|
||||
def __gt__(self, other):
|
||||
if not isinstance(other, Image):
|
||||
return True
|
||||
return (self.language != other.language) \
|
||||
and (other.language == self._locale.language)
|
||||
|
||||
# direct match for comparison
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Image):
|
||||
return False
|
||||
return self.filename == other.filename
|
||||
|
||||
# special handling for boolean to see if exists
|
||||
def __nonzero__(self):
|
||||
if len(self.filename) == 0:
|
||||
@@ -228,20 +274,28 @@ class Image( Element ):
|
||||
# BASE62 encoded filename, no need to worry about unicode
|
||||
return u"<{0.__class__.__name__} '{0.filename}'>".format(self)
|
||||
|
||||
class Backdrop( Image ):
|
||||
|
||||
class Backdrop(Image):
|
||||
def sizes(self):
|
||||
return Configuration.images['backdrop_sizes']
|
||||
class Poster( Image ):
|
||||
|
||||
|
||||
class Poster(Image):
|
||||
def sizes(self):
|
||||
return Configuration.images['poster_sizes']
|
||||
class Profile( Image ):
|
||||
|
||||
|
||||
class Profile(Image):
|
||||
def sizes(self):
|
||||
return Configuration.images['profile_sizes']
|
||||
class Logo( Image ):
|
||||
|
||||
|
||||
class Logo(Image):
|
||||
def sizes(self):
|
||||
return Configuration.images['logo_sizes']
|
||||
|
||||
class AlternateTitle( Element ):
|
||||
|
||||
class AlternateTitle(Element):
|
||||
country = Datapoint('iso_3166_1')
|
||||
title = Datapoint('title')
|
||||
|
||||
@@ -249,28 +303,31 @@ class AlternateTitle( Element ):
|
||||
def __lt__(self, other):
|
||||
return (self.country == self._locale.country) \
|
||||
and (self.country != other.country)
|
||||
|
||||
def __gt__(self, other):
|
||||
return (self.country != other.country) \
|
||||
and (other.country == self._locale.country)
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.country == other.country
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.title}' ({0.country})>"\
|
||||
.format(self).encode('utf-8')
|
||||
.format(self).encode('utf-8')
|
||||
|
||||
class Person( Element ):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
biography = Datapoint('biography')
|
||||
dayofbirth = Datapoint('birthday', default=None, handler=process_date)
|
||||
dayofdeath = Datapoint('deathday', default=None, handler=process_date)
|
||||
homepage = Datapoint('homepage')
|
||||
birthplace = Datapoint('place_of_birth')
|
||||
profile = Datapoint('profile_path', handler=Profile, \
|
||||
raw=False, default=None)
|
||||
adult = Datapoint('adult')
|
||||
aliases = Datalist('also_known_as')
|
||||
|
||||
class Person(Element):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
biography = Datapoint('biography')
|
||||
dayofbirth = Datapoint('birthday', default=None, handler=process_date)
|
||||
dayofdeath = Datapoint('deathday', default=None, handler=process_date)
|
||||
homepage = Datapoint('homepage')
|
||||
birthplace = Datapoint('place_of_birth')
|
||||
profile = Datapoint('profile_path', handler=Profile,
|
||||
raw=False, default=None)
|
||||
adult = Datapoint('adult')
|
||||
aliases = Datalist('also_known_as')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.name}'>"\
|
||||
@@ -278,55 +335,63 @@ class Person( Element ):
|
||||
|
||||
def _populate(self):
|
||||
return Request('person/{0}'.format(self.id))
|
||||
|
||||
def _populate_credits(self):
|
||||
return Request('person/{0}/credits'.format(self.id), \
|
||||
language=self._locale.language)
|
||||
return Request('person/{0}/credits'.format(self.id),
|
||||
language=self._locale.language)
|
||||
def _populate_images(self):
|
||||
return Request('person/{0}/images'.format(self.id))
|
||||
|
||||
roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x), \
|
||||
poller=_populate_credits)
|
||||
crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x), \
|
||||
poller=_populate_credits)
|
||||
profiles = Datalist('profiles', handler=Profile, poller=_populate_images)
|
||||
roles = Datalist('cast', handler=lambda x: ReverseCast(raw=x),
|
||||
poller=_populate_credits)
|
||||
crew = Datalist('crew', handler=lambda x: ReverseCrew(raw=x),
|
||||
poller=_populate_credits)
|
||||
profiles = Datalist('profiles', handler=Profile, poller=_populate_images)
|
||||
|
||||
class Cast( Person ):
|
||||
character = Datapoint('character')
|
||||
order = Datapoint('order')
|
||||
|
||||
class Cast(Person):
|
||||
character = Datapoint('character')
|
||||
order = Datapoint('order')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.name}' as '{0.character}'>"\
|
||||
.format(self).encode('utf-8')
|
||||
.format(self).encode('utf-8')
|
||||
|
||||
class Crew( Person ):
|
||||
job = Datapoint('job')
|
||||
department = Datapoint('department')
|
||||
|
||||
class Crew(Person):
|
||||
job = Datapoint('job')
|
||||
department = Datapoint('department')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.name}','{0.job}'>"\
|
||||
.format(self).encode('utf-8')
|
||||
.format(self).encode('utf-8')
|
||||
|
||||
class Keyword( Element ):
|
||||
|
||||
class Keyword(Element):
|
||||
id = Datapoint('id')
|
||||
name = Datapoint('name')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} {0.name}>".format(self).encode('utf-8')
|
||||
return u"<{0.__class__.__name__} {0.name}>"\
|
||||
.format(self).encode('utf-8')
|
||||
|
||||
class Release( Element ):
|
||||
certification = Datapoint('certification')
|
||||
country = Datapoint('iso_3166_1')
|
||||
releasedate = Datapoint('release_date', handler=process_date)
|
||||
|
||||
class Release(Element):
|
||||
certification = Datapoint('certification')
|
||||
country = Datapoint('iso_3166_1')
|
||||
releasedate = Datapoint('release_date', handler=process_date)
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} {0.country}, {0.releasedate}>"\
|
||||
.format(self).encode('utf-8')
|
||||
.format(self).encode('utf-8')
|
||||
|
||||
class Trailer( Element ):
|
||||
name = Datapoint('name')
|
||||
size = Datapoint('size')
|
||||
source = Datapoint('source')
|
||||
|
||||
class YoutubeTrailer( Trailer ):
|
||||
class Trailer(Element):
|
||||
name = Datapoint('name')
|
||||
size = Datapoint('size')
|
||||
source = Datapoint('source')
|
||||
|
||||
|
||||
class YoutubeTrailer(Trailer):
|
||||
def geturl(self):
|
||||
return "http://www.youtube.com/watch?v={0}".format(self.source)
|
||||
|
||||
@@ -334,8 +399,9 @@ class YoutubeTrailer( Trailer ):
|
||||
# modified BASE64 encoding, no need to worry about unicode
|
||||
return u"<{0.__class__.__name__} '{0.name}'>".format(self)
|
||||
|
||||
class AppleTrailer( Element ):
|
||||
name = Datapoint('name')
|
||||
|
||||
class AppleTrailer(Element):
|
||||
name = Datapoint('name')
|
||||
sources = Datadict('sources', handler=Trailer, attr='size')
|
||||
|
||||
def sizes(self):
|
||||
@@ -344,84 +410,91 @@ class AppleTrailer( Element ):
|
||||
def geturl(self, size=None):
|
||||
if size is None:
|
||||
# sort assuming ###p format for now, take largest resolution
|
||||
size = str(sorted([int(size[:-1]) for size in self.sources])[-1])+'p'
|
||||
size = str(sorted(
|
||||
[int(size[:-1]) for size in self.sources]
|
||||
)[-1]) + 'p'
|
||||
return self.sources[size].source
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.name}'>".format(self)
|
||||
|
||||
class Translation( Element ):
|
||||
name = Datapoint('name')
|
||||
language = Datapoint('iso_639_1')
|
||||
englishname = Datapoint('english_name')
|
||||
|
||||
class Translation(Element):
|
||||
name = Datapoint('name')
|
||||
language = Datapoint('iso_639_1')
|
||||
englishname = Datapoint('english_name')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.name}' ({0.language})>"\
|
||||
.format(self).encode('utf-8')
|
||||
.format(self).encode('utf-8')
|
||||
|
||||
class Genre( NameRepr, Element ):
|
||||
id = Datapoint('id')
|
||||
name = Datapoint('name')
|
||||
|
||||
class Genre(NameRepr, Element):
|
||||
id = Datapoint('id')
|
||||
name = Datapoint('name')
|
||||
|
||||
def _populate_movies(self):
|
||||
return Request('genre/{0}/movies'.format(self.id), \
|
||||
language=self._locale.language)
|
||||
language=self._locale.language)
|
||||
|
||||
@property
|
||||
def movies(self):
|
||||
if 'movies' not in self._data:
|
||||
search = MovieSearchResult(self._populate_movies(), \
|
||||
locale=self._locale)
|
||||
locale=self._locale)
|
||||
search._name = "{0.name} Movies".format(self)
|
||||
self._data['movies'] = search
|
||||
return self._data['movies']
|
||||
|
||||
@classmethod
|
||||
def getAll(cls, locale=None):
|
||||
class GenreList( Element ):
|
||||
class GenreList(Element):
|
||||
genres = Datalist('genres', handler=Genre)
|
||||
|
||||
def _populate(self):
|
||||
return Request('genre/list', language=self._locale.language)
|
||||
return GenreList(locale=locale).genres
|
||||
|
||||
|
||||
class Studio( NameRepr, Element ):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
description = Datapoint('description')
|
||||
headquarters = Datapoint('headquarters')
|
||||
logo = Datapoint('logo_path', handler=Logo, \
|
||||
raw=False, default=None)
|
||||
|
||||
class Studio(NameRepr, Element):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
description = Datapoint('description')
|
||||
headquarters = Datapoint('headquarters')
|
||||
logo = Datapoint('logo_path', handler=Logo, raw=False, default=None)
|
||||
# FIXME: manage not-yet-defined handlers in a way that will propogate
|
||||
# locale information properly
|
||||
parent = Datapoint('parent_company', \
|
||||
handler=lambda x: Studio(raw=x))
|
||||
parent = Datapoint('parent_company', handler=lambda x: Studio(raw=x))
|
||||
|
||||
def _populate(self):
|
||||
return Request('company/{0}'.format(self.id))
|
||||
|
||||
def _populate_movies(self):
|
||||
return Request('company/{0}/movies'.format(self.id), \
|
||||
language=self._locale.language)
|
||||
return Request('company/{0}/movies'.format(self.id),
|
||||
language=self._locale.language)
|
||||
|
||||
# FIXME: add a cleaner way of adding types with no additional processing
|
||||
@property
|
||||
def movies(self):
|
||||
if 'movies' not in self._data:
|
||||
search = MovieSearchResult(self._populate_movies(), \
|
||||
locale=self._locale)
|
||||
search = MovieSearchResult(self._populate_movies(),
|
||||
locale=self._locale)
|
||||
search._name = "{0.name} Movies".format(self)
|
||||
self._data['movies'] = search
|
||||
return self._data['movies']
|
||||
|
||||
class Country( NameRepr, Element ):
|
||||
code = Datapoint('iso_3166_1')
|
||||
name = Datapoint('name')
|
||||
|
||||
class Language( NameRepr, Element ):
|
||||
code = Datapoint('iso_639_1')
|
||||
name = Datapoint('name')
|
||||
class Country(NameRepr, Element):
|
||||
code = Datapoint('iso_3166_1')
|
||||
name = Datapoint('name')
|
||||
|
||||
class Movie( Element ):
|
||||
|
||||
class Language(NameRepr, Element):
|
||||
code = Datapoint('iso_639_1')
|
||||
name = Datapoint('name')
|
||||
|
||||
|
||||
class Movie(Element):
|
||||
@classmethod
|
||||
def latest(cls):
|
||||
req = Request('latest/movie')
|
||||
@@ -459,7 +532,7 @@ class Movie( Element ):
|
||||
account = Account(session=session)
|
||||
res = MovieSearchResult(
|
||||
Request('account/{0}/favorite_movies'.format(account.id),
|
||||
session_id=session.sessionid))
|
||||
session_id=session.sessionid))
|
||||
res._name = "Favorites"
|
||||
return res
|
||||
|
||||
@@ -470,7 +543,7 @@ class Movie( Element ):
|
||||
account = Account(session=session)
|
||||
res = MovieSearchResult(
|
||||
Request('account/{0}/rated_movies'.format(account.id),
|
||||
session_id=session.sessionid))
|
||||
session_id=session.sessionid))
|
||||
res._name = "Movies You Rated"
|
||||
return res
|
||||
|
||||
@@ -481,7 +554,7 @@ class Movie( Element ):
|
||||
account = Account(session=session)
|
||||
res = MovieSearchResult(
|
||||
Request('account/{0}/movie_watchlist'.format(account.id),
|
||||
session_id=session.sessionid))
|
||||
session_id=session.sessionid))
|
||||
res._name = "Movies You're Watching"
|
||||
return res
|
||||
|
||||
@@ -500,104 +573,116 @@ class Movie( Element ):
|
||||
movie._populate()
|
||||
return movie
|
||||
|
||||
id = Datapoint('id', initarg=1)
|
||||
title = Datapoint('title')
|
||||
originaltitle = Datapoint('original_title')
|
||||
tagline = Datapoint('tagline')
|
||||
overview = Datapoint('overview')
|
||||
runtime = Datapoint('runtime')
|
||||
budget = Datapoint('budget')
|
||||
revenue = Datapoint('revenue')
|
||||
releasedate = Datapoint('release_date', handler=process_date)
|
||||
homepage = Datapoint('homepage')
|
||||
imdb = Datapoint('imdb_id')
|
||||
id = Datapoint('id', initarg=1)
|
||||
title = Datapoint('title')
|
||||
originaltitle = Datapoint('original_title')
|
||||
tagline = Datapoint('tagline')
|
||||
overview = Datapoint('overview')
|
||||
runtime = Datapoint('runtime')
|
||||
budget = Datapoint('budget')
|
||||
revenue = Datapoint('revenue')
|
||||
releasedate = Datapoint('release_date', handler=process_date)
|
||||
homepage = Datapoint('homepage')
|
||||
imdb = Datapoint('imdb_id')
|
||||
|
||||
backdrop = Datapoint('backdrop_path', handler=Backdrop, \
|
||||
raw=False, default=None)
|
||||
poster = Datapoint('poster_path', handler=Poster, \
|
||||
raw=False, default=None)
|
||||
backdrop = Datapoint('backdrop_path', handler=Backdrop,
|
||||
raw=False, default=None)
|
||||
poster = Datapoint('poster_path', handler=Poster,
|
||||
raw=False, default=None)
|
||||
|
||||
popularity = Datapoint('popularity')
|
||||
userrating = Datapoint('vote_average')
|
||||
votes = Datapoint('vote_count')
|
||||
popularity = Datapoint('popularity')
|
||||
userrating = Datapoint('vote_average')
|
||||
votes = Datapoint('vote_count')
|
||||
|
||||
adult = Datapoint('adult')
|
||||
collection = Datapoint('belongs_to_collection', handler=lambda x: \
|
||||
adult = Datapoint('adult')
|
||||
collection = Datapoint('belongs_to_collection', handler=lambda x: \
|
||||
Collection(raw=x))
|
||||
genres = Datalist('genres', handler=Genre)
|
||||
studios = Datalist('production_companies', handler=Studio)
|
||||
countries = Datalist('production_countries', handler=Country)
|
||||
languages = Datalist('spoken_languages', handler=Language)
|
||||
genres = Datalist('genres', handler=Genre)
|
||||
studios = Datalist('production_companies', handler=Studio)
|
||||
countries = Datalist('production_countries', handler=Country)
|
||||
languages = Datalist('spoken_languages', handler=Language)
|
||||
|
||||
def _populate(self):
|
||||
return Request('movie/{0}'.format(self.id), \
|
||||
language=self._locale.language)
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_titles(self):
|
||||
kwargs = {}
|
||||
if not self._locale.fallthrough:
|
||||
kwargs['country'] = self._locale.country
|
||||
return Request('movie/{0}/alternative_titles'.format(self.id), **kwargs)
|
||||
return Request('movie/{0}/alternative_titles'.format(self.id),
|
||||
**kwargs)
|
||||
|
||||
def _populate_cast(self):
|
||||
return Request('movie/{0}/casts'.format(self.id))
|
||||
|
||||
def _populate_images(self):
|
||||
kwargs = {}
|
||||
if not self._locale.fallthrough:
|
||||
kwargs['language'] = self._locale.language
|
||||
return Request('movie/{0}/images'.format(self.id), **kwargs)
|
||||
|
||||
def _populate_keywords(self):
|
||||
return Request('movie/{0}/keywords'.format(self.id))
|
||||
|
||||
def _populate_releases(self):
|
||||
return Request('movie/{0}/releases'.format(self.id))
|
||||
|
||||
def _populate_trailers(self):
|
||||
return Request('movie/{0}/trailers'.format(self.id), \
|
||||
return Request('movie/{0}/trailers'.format(self.id),
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_translations(self):
|
||||
return Request('movie/{0}/translations'.format(self.id))
|
||||
|
||||
alternate_titles = Datalist('titles', handler=AlternateTitle, \
|
||||
poller=_populate_titles, sort=True)
|
||||
cast = Datalist('cast', handler=Cast, \
|
||||
poller=_populate_cast, sort='order')
|
||||
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
|
||||
backdrops = Datalist('backdrops', handler=Backdrop, \
|
||||
poller=_populate_images, sort=True)
|
||||
posters = Datalist('posters', handler=Poster, \
|
||||
poller=_populate_images, sort=True)
|
||||
keywords = Datalist('keywords', handler=Keyword, \
|
||||
poller=_populate_keywords)
|
||||
releases = Datadict('countries', handler=Release, \
|
||||
poller=_populate_releases, attr='country')
|
||||
youtube_trailers = Datalist('youtube', handler=YoutubeTrailer, \
|
||||
poller=_populate_trailers)
|
||||
apple_trailers = Datalist('quicktime', handler=AppleTrailer, \
|
||||
poller=_populate_trailers)
|
||||
translations = Datalist('translations', handler=Translation, \
|
||||
poller=_populate_translations)
|
||||
poller=_populate_titles, sort=True)
|
||||
|
||||
# FIXME: this data point will need to be changed to 'credits' at some point
|
||||
cast = Datalist('cast', handler=Cast,
|
||||
poller=_populate_cast, sort='order')
|
||||
|
||||
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
|
||||
backdrops = Datalist('backdrops', handler=Backdrop,
|
||||
poller=_populate_images, sort=True)
|
||||
posters = Datalist('posters', handler=Poster,
|
||||
poller=_populate_images, sort=True)
|
||||
keywords = Datalist('keywords', handler=Keyword,
|
||||
poller=_populate_keywords)
|
||||
releases = Datadict('countries', handler=Release,
|
||||
poller=_populate_releases, attr='country')
|
||||
youtube_trailers = Datalist('youtube', handler=YoutubeTrailer,
|
||||
poller=_populate_trailers)
|
||||
apple_trailers = Datalist('quicktime', handler=AppleTrailer,
|
||||
poller=_populate_trailers)
|
||||
translations = Datalist('translations', handler=Translation,
|
||||
poller=_populate_translations)
|
||||
|
||||
def setFavorite(self, value):
|
||||
req = Request('account/{0}/favorite'.format(\
|
||||
Account(session=self._session).id),
|
||||
session_id=self._session.sessionid)
|
||||
req.add_data({'movie_id':self.id, 'favorite':str(bool(value)).lower()})
|
||||
req = Request('account/{0}/favorite'.format(
|
||||
Account(session=self._session).id),
|
||||
session_id=self._session.sessionid)
|
||||
req.add_data({'movie_id': self.id,
|
||||
'favorite': str(bool(value)).lower()})
|
||||
req.lifetime = 0
|
||||
req.readJSON()
|
||||
|
||||
def setRating(self, value):
|
||||
if not (0 <= value <= 10):
|
||||
raise TMDBError("Ratings must be between '0' and '10'.")
|
||||
req = Request('movie/{0}/rating'.format(self.id), \
|
||||
session_id=self._session.sessionid)
|
||||
req = Request('movie/{0}/rating'.format(self.id),
|
||||
session_id=self._session.sessionid)
|
||||
req.lifetime = 0
|
||||
req.add_data({'value':value})
|
||||
req.readJSON()
|
||||
|
||||
def setWatchlist(self, value):
|
||||
req = Request('account/{0}/movie_watchlist'.format(\
|
||||
Account(session=self._session).id),
|
||||
session_id=self._session.sessionid)
|
||||
req = Request('account/{0}/movie_watchlist'.format(
|
||||
Account(session=self._session).id),
|
||||
session_id=self._session.sessionid)
|
||||
req.lifetime = 0
|
||||
req.add_data({'movie_id':self.id,
|
||||
'movie_watchlist':str(bool(value)).lower()})
|
||||
req.add_data({'movie_id': self.id,
|
||||
'movie_watchlist': str(bool(value)).lower()})
|
||||
req.readJSON()
|
||||
|
||||
def getSimilar(self):
|
||||
@@ -605,9 +690,9 @@ class Movie( Element ):
|
||||
|
||||
@property
|
||||
def similar(self):
|
||||
res = MovieSearchResult(Request('movie/{0}/similar_movies'\
|
||||
.format(self.id)),
|
||||
locale=self._locale)
|
||||
res = MovieSearchResult(Request(
|
||||
'movie/{0}/similar_movies'.format(self.id)),
|
||||
locale=self._locale)
|
||||
res._name = 'Similar to {0}'.format(self._printable_name())
|
||||
return res
|
||||
|
||||
@@ -629,61 +714,197 @@ class Movie( Element ):
|
||||
return s
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0} {1}>".format(self.__class__.__name__,\
|
||||
return u"<{0} {1}>".format(self.__class__.__name__,
|
||||
self._printable_name()).encode('utf-8')
|
||||
|
||||
|
||||
class ReverseCast( Movie ):
|
||||
character = Datapoint('character')
|
||||
character = Datapoint('character')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.character}' on {1}>"\
|
||||
.format(self, self._printable_name()).encode('utf-8')
|
||||
return (u"<{0.__class__.__name__} '{0.character}' on {1}>"
|
||||
.format(self, self._printable_name()).encode('utf-8'))
|
||||
|
||||
|
||||
class ReverseCrew( Movie ):
|
||||
department = Datapoint('department')
|
||||
job = Datapoint('job')
|
||||
department = Datapoint('department')
|
||||
job = Datapoint('job')
|
||||
|
||||
def __repr__(self):
|
||||
return u"<{0.__class__.__name__} '{0.job}' for {1}>"\
|
||||
.format(self, self._printable_name()).encode('utf-8')
|
||||
return (u"<{0.__class__.__name__} '{0.job}' for {1}>"
|
||||
.format(self, self._printable_name()).encode('utf-8'))
|
||||
|
||||
class Collection( NameRepr, Element ):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
|
||||
class Collection(NameRepr, Element):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
backdrop = Datapoint('backdrop_path', handler=Backdrop, \
|
||||
raw=False, default=None)
|
||||
poster = Datapoint('poster_path', handler=Poster, \
|
||||
raw=False, default=None)
|
||||
members = Datalist('parts', handler=Movie)
|
||||
raw=False, default=None)
|
||||
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
|
||||
members = Datalist('parts', handler=Movie)
|
||||
overview = Datapoint('overview')
|
||||
|
||||
def _populate(self):
|
||||
return Request('collection/{0}'.format(self.id), \
|
||||
language=self._locale.language)
|
||||
return Request('collection/{0}'.format(self.id),
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_images(self):
|
||||
kwargs = {}
|
||||
if not self._locale.fallthrough:
|
||||
kwargs['language'] = self._locale.language
|
||||
return Request('collection/{0}/images'.format(self.id), **kwargs)
|
||||
|
||||
backdrops = Datalist('backdrops', handler=Backdrop, \
|
||||
poller=_populate_images, sort=True)
|
||||
posters = Datalist('posters', handler=Poster, \
|
||||
poller=_populate_images, sort=True)
|
||||
backdrops = Datalist('backdrops', handler=Backdrop,
|
||||
poller=_populate_images, sort=True)
|
||||
posters = Datalist('posters', handler=Poster,
|
||||
poller=_populate_images, sort=True)
|
||||
|
||||
class List( NameRepr, Element ):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
author = Datapoint('created_by')
|
||||
class List(NameRepr, Element):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
author = Datapoint('created_by')
|
||||
description = Datapoint('description')
|
||||
favorites = Datapoint('favorite_count')
|
||||
language = Datapoint('iso_639_1')
|
||||
count = Datapoint('item_count')
|
||||
poster = Datapoint('poster_path', handler=Poster, \
|
||||
raw=False, default=None)
|
||||
|
||||
members = Datalist('items', handler=Movie)
|
||||
favorites = Datapoint('favorite_count')
|
||||
language = Datapoint('iso_639_1')
|
||||
count = Datapoint('item_count')
|
||||
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
|
||||
members = Datalist('items', handler=Movie)
|
||||
|
||||
def _populate(self):
|
||||
return Request('list/{0}'.format(self.id))
|
||||
|
||||
class Network(NameRepr,Element):
|
||||
id = Datapoint('id', initarg=1)
|
||||
name = Datapoint('name')
|
||||
|
||||
class Episode(NameRepr, Element):
|
||||
episode_number = Datapoint('episode_number', initarg=3)
|
||||
season_number = Datapoint('season_number', initarg=2)
|
||||
series_id = Datapoint('series_id', initarg=1)
|
||||
air_date = Datapoint('air_date', handler=process_date)
|
||||
overview = Datapoint('overview')
|
||||
name = Datapoint('name')
|
||||
userrating = Datapoint('vote_average')
|
||||
votes = Datapoint('vote_count')
|
||||
id = Datapoint('id')
|
||||
production_code = Datapoint('production_code')
|
||||
still = Datapoint('still_path', handler=Backdrop, raw=False, default=None)
|
||||
|
||||
def _populate(self):
|
||||
return Request('tv/{0}/season/{1}/episode/{2}'.format(self.series_id, self.season_number, self.episode_number),
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_cast(self):
|
||||
return Request('tv/{0}/season/{1}/episode/{2}/credits'.format(
|
||||
self.series_id, self.season_number, self.episode_number),
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_external_ids(self):
|
||||
return Request('tv/{0}/season/{1}/episode/{2}/external_ids'.format(
|
||||
self.series_id, self.season_number, self.episode_number))
|
||||
|
||||
def _populate_images(self):
|
||||
kwargs = {}
|
||||
if not self._locale.fallthrough:
|
||||
kwargs['language'] = self._locale.language
|
||||
return Request('tv/{0}/season/{1}/episode/{2}/images'.format(
|
||||
self.series_id, self.season_number, self.episode_number), **kwargs)
|
||||
|
||||
cast = Datalist('cast', handler=Cast,
|
||||
poller=_populate_cast, sort='order')
|
||||
guest_stars = Datalist('guest_stars', handler=Cast,
|
||||
poller=_populate_cast, sort='order')
|
||||
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
|
||||
imdb_id = Datapoint('imdb_id', poller=_populate_external_ids)
|
||||
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
|
||||
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
|
||||
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
|
||||
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
|
||||
stills = Datalist('stills', handler=Backdrop, poller=_populate_images, sort=True)
|
||||
|
||||
class Season(NameRepr, Element):
|
||||
season_number = Datapoint('season_number', initarg=2)
|
||||
series_id = Datapoint('series_id', initarg=1)
|
||||
id = Datapoint('id')
|
||||
air_date = Datapoint('air_date', handler=process_date)
|
||||
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
|
||||
overview = Datapoint('overview')
|
||||
name = Datapoint('name')
|
||||
episodes = Datadict('episodes', attr='episode_number', handler=Episode,
|
||||
passthrough={'series_id': 'series_id', 'season_number': 'season_number'})
|
||||
|
||||
def _populate(self):
|
||||
return Request('tv/{0}/season/{1}'.format(self.series_id, self.season_number),
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_images(self):
|
||||
kwargs = {}
|
||||
if not self._locale.fallthrough:
|
||||
kwargs['language'] = self._locale.language
|
||||
return Request('tv/{0}/season/{1}/images'.format(self.series_id, self.season_number), **kwargs)
|
||||
|
||||
def _populate_external_ids(self):
|
||||
return Request('tv/{0}/season/{1}/external_ids'.format(self.series_id, self.season_number))
|
||||
|
||||
posters = Datalist('posters', handler=Poster,
|
||||
poller=_populate_images, sort=True)
|
||||
|
||||
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
|
||||
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
|
||||
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
|
||||
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
|
||||
|
||||
class Series(NameRepr, Element):
|
||||
id = Datapoint('id', initarg=1)
|
||||
backdrop = Datapoint('backdrop_path', handler=Backdrop, raw=False, default=None)
|
||||
authors = Datalist('created_by', handler=Person)
|
||||
episode_run_times = Datalist('episode_run_time')
|
||||
first_air_date = Datapoint('first_air_date', handler=process_date)
|
||||
last_air_date = Datapoint('last_air_date', handler=process_date)
|
||||
genres = Datalist('genres', handler=Genre)
|
||||
homepage = Datapoint('homepage')
|
||||
in_production = Datapoint('in_production')
|
||||
languages = Datalist('languages')
|
||||
origin_countries = Datalist('origin_country')
|
||||
name = Datapoint('name')
|
||||
original_name = Datapoint('original_name')
|
||||
number_of_episodes = Datapoint('number_of_episodes')
|
||||
number_of_seasons = Datapoint('number_of_seasons')
|
||||
overview = Datapoint('overview')
|
||||
popularity = Datapoint('popularity')
|
||||
status = Datapoint('status')
|
||||
userrating = Datapoint('vote_average')
|
||||
votes = Datapoint('vote_count')
|
||||
poster = Datapoint('poster_path', handler=Poster, raw=False, default=None)
|
||||
networks = Datalist('networks', handler=Network)
|
||||
seasons = Datadict('seasons', attr='season_number', handler=Season, passthrough={'id': 'series_id'})
|
||||
|
||||
def _populate(self):
|
||||
return Request('tv/{0}'.format(self.id),
|
||||
language=self._locale.language)
|
||||
|
||||
def _populate_cast(self):
|
||||
return Request('tv/{0}/credits'.format(self.id))
|
||||
|
||||
def _populate_images(self):
|
||||
kwargs = {}
|
||||
if not self._locale.fallthrough:
|
||||
kwargs['language'] = self._locale.language
|
||||
return Request('tv/{0}/images'.format(self.id), **kwargs)
|
||||
|
||||
def _populate_external_ids(self):
|
||||
return Request('tv/{0}/external_ids'.format(self.id))
|
||||
|
||||
cast = Datalist('cast', handler=Cast,
|
||||
poller=_populate_cast, sort='order')
|
||||
crew = Datalist('crew', handler=Crew, poller=_populate_cast)
|
||||
backdrops = Datalist('backdrops', handler=Backdrop,
|
||||
poller=_populate_images, sort=True)
|
||||
posters = Datalist('posters', handler=Poster,
|
||||
poller=_populate_images, sort=True)
|
||||
|
||||
imdb_id = Datapoint('imdb_id', poller=_populate_external_ids)
|
||||
freebase_id = Datapoint('freebase_id', poller=_populate_external_ids)
|
||||
freebase_mid = Datapoint('freebase_mid', poller=_populate_external_ids)
|
||||
tvdb_id = Datapoint('tvdb_id', poller=_populate_external_ids)
|
||||
tvrage_id = Datapoint('tvrage_id', poller=_populate_external_ids)
|
||||
|
||||
@@ -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<year>[0-9]{4})'
|
||||
'-(?P<month>[0-9]{1,2})'
|
||||
@@ -27,21 +27,27 @@ class datetime( _pydatetime ):
|
||||
'(?P<tzmin>[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
|
||||
|
||||
|
||||
@@ -6,23 +6,24 @@
|
||||
# Author: Raymond Wagner
|
||||
#-----------------------
|
||||
|
||||
class TMDBError( Exception ):
|
||||
Error = 0
|
||||
KeyError = 10
|
||||
KeyMissing = 20
|
||||
KeyInvalid = 30
|
||||
KeyRevoked = 40
|
||||
RequestError = 50
|
||||
RequestInvalid = 51
|
||||
PagingIssue = 60
|
||||
CacheError = 70
|
||||
CacheReadError = 71
|
||||
CacheWriteError = 72
|
||||
CacheDirectoryError = 73
|
||||
ImageSizeError = 80
|
||||
HTTPError = 90
|
||||
Offline = 100
|
||||
LocaleError = 110
|
||||
|
||||
class TMDBError(Exception):
|
||||
Error = 0
|
||||
KeyError = 10
|
||||
KeyMissing = 20
|
||||
KeyInvalid = 30
|
||||
KeyRevoked = 40
|
||||
RequestError = 50
|
||||
RequestInvalid = 51
|
||||
PagingIssue = 60
|
||||
CacheError = 70
|
||||
CacheReadError = 71
|
||||
CacheWriteError = 72
|
||||
CacheDirectoryError = 73
|
||||
ImageSizeError = 80
|
||||
HTTPError = 90
|
||||
Offline = 100
|
||||
LocaleError = 110
|
||||
|
||||
def __init__(self, msg=None, errno=0):
|
||||
self.errno = errno
|
||||
@@ -30,60 +31,77 @@ class TMDBError( Exception ):
|
||||
self.errno = getattr(self, 'TMDB'+self.__class__.__name__, errno)
|
||||
self.args = (msg,)
|
||||
|
||||
class TMDBKeyError( TMDBError ):
|
||||
|
||||
class TMDBKeyError(TMDBError):
|
||||
pass
|
||||
|
||||
class TMDBKeyMissing( TMDBKeyError ):
|
||||
|
||||
class TMDBKeyMissing(TMDBKeyError):
|
||||
pass
|
||||
|
||||
class TMDBKeyInvalid( TMDBKeyError ):
|
||||
|
||||
class TMDBKeyInvalid(TMDBKeyError):
|
||||
pass
|
||||
|
||||
class TMDBKeyRevoked( TMDBKeyInvalid ):
|
||||
|
||||
class TMDBKeyRevoked(TMDBKeyInvalid):
|
||||
pass
|
||||
|
||||
class TMDBRequestError( TMDBError ):
|
||||
|
||||
class TMDBRequestError(TMDBError):
|
||||
pass
|
||||
|
||||
class TMDBRequestInvalid( TMDBRequestError ):
|
||||
|
||||
class TMDBRequestInvalid(TMDBRequestError):
|
||||
pass
|
||||
|
||||
class TMDBPagingIssue( TMDBRequestError ):
|
||||
|
||||
class TMDBPagingIssue(TMDBRequestError):
|
||||
pass
|
||||
|
||||
class TMDBCacheError( TMDBRequestError ):
|
||||
|
||||
class TMDBCacheError(TMDBRequestError):
|
||||
pass
|
||||
|
||||
class TMDBCacheReadError( TMDBCacheError ):
|
||||
|
||||
class TMDBCacheReadError(TMDBCacheError):
|
||||
def __init__(self, filename):
|
||||
super(TMDBCacheReadError, self).__init__(
|
||||
"User does not have permission to access cache file: {0}.".format(filename))
|
||||
"User does not have permission to access cache file: {0}."\
|
||||
.format(filename))
|
||||
self.filename = filename
|
||||
|
||||
class TMDBCacheWriteError( TMDBCacheError ):
|
||||
|
||||
class TMDBCacheWriteError(TMDBCacheError):
|
||||
def __init__(self, filename):
|
||||
super(TMDBCacheWriteError, self).__init__(
|
||||
"User does not have permission to write cache file: {0}.".format(filename))
|
||||
"User does not have permission to write cache file: {0}."\
|
||||
.format(filename))
|
||||
self.filename = filename
|
||||
|
||||
class TMDBCacheDirectoryError( TMDBCacheError ):
|
||||
|
||||
class TMDBCacheDirectoryError(TMDBCacheError):
|
||||
def __init__(self, filename):
|
||||
super(TMDBCacheDirectoryError, self).__init__(
|
||||
"Directory containing cache file does not exist: {0}.".format(filename))
|
||||
"Directory containing cache file does not exist: {0}."\
|
||||
.format(filename))
|
||||
self.filename = filename
|
||||
|
||||
class TMDBImageSizeError( TMDBError ):
|
||||
|
||||
class TMDBImageSizeError(TMDBError ):
|
||||
pass
|
||||
|
||||
class TMDBHTTPError( TMDBError ):
|
||||
|
||||
class TMDBHTTPError(TMDBError):
|
||||
def __init__(self, err):
|
||||
self.httperrno = err.code
|
||||
self.response = err.fp.read()
|
||||
super(TMDBHTTPError, self).__init__(str(err))
|
||||
|
||||
class TMDBOffline( TMDBError ):
|
||||
|
||||
class TMDBOffline(TMDBError):
|
||||
pass
|
||||
|
||||
class TMDBLocaleError( TMDBError ):
|
||||
pass
|
||||
|
||||
class TMDBLocaleError(TMDBError):
|
||||
pass
|
||||
|
||||
@@ -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"<Search Results: {0}>".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'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user