Notifications

This commit is contained in:
Ruud committed 2011-04-09 20:18:51 +02:00
1 parent 288aa8c0a1
commit 85fc1c01ee
67 files changed
+3270 -400

No files matched your search

+3 -13
View File
@@ -9,35 +9,28 @@ config = [{
{
'tab': 'general',
'name': 'basics',
'label': 'Basics',
'description': 'Needs restart before changes take effect.',
'options': [
{
'name': 'username',
'default': '',
'type': 'string',
'label': 'Username',
},
{
'name': 'password',
'default': '',
'password': True,
'type': 'string',
'label': 'Password',
'type': 'password',
},
{
'name': 'host',
'advanced': True,
'default': '0.0.0.0',
'type': 'string',
'label': 'Host',
'label': 'IP',
'description': 'Host that I should listen to. "0.0.0.0" listens to all ips.',
},
{
'name': 'port',
'default': 5000,
'type': 'int',
'label': 'Port',
'description': 'The port I should listen to.',
},
{
@@ -52,14 +45,12 @@ config = [{
{
'tab': 'general',
'name': 'advanced',
'label': 'Advanced',
'description': "For those who know what the're doing",
'advanced': True,
'options': [
{
'name': 'api_key',
'default': uuid4().hex,
'type': 'string',
'readonly': True,
'label': 'Api Key',
'description': "This is top-secret! Don't share this!",
@@ -74,9 +65,8 @@ config = [{
{
'name': 'url_base',
'default': '',
'type': 'string',
'label': 'Url Base',
'description': 'When using mod_proxy use this to prepend the url with this.',
'description': 'When using mod_proxy use this to append the url with this.',
},
],
},
Whitespace-only changes.
+20
View File
@@ -0,0 +1,20 @@
from couchpotato.core.event import addEvent
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
class Downloader(Plugin):
def __init__(self):
addEvent('download', self.download)
def download(self, data = {}):
pass
def conf(self, attr):
return Env.setting(attr, self.__class__.__name__.lower())
def isDisabled(self):
return not self.isEnabled()
def isEnabled(self):
return self.conf('enabled', True)
@@ -0,0 +1,32 @@
from .main import Blackhole
def start():
return Blackhole()
config = [{
'name': 'blackhole',
'groups': [
{
'tab': 'downloaders',
'name': 'blackhole',
'label': 'Black hole',
'description': 'Fill in your Sabnzbd settings.',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'bool',
'label': 'Enabled',
'description': 'Send snatched NZBs to Sabnzbd',
},
{
'name': 'directory',
'default': '',
'type': 'directory',
'label': 'Directory',
'description': 'Directory where the .nzb (or .torrent) file is saved to.',
},
],
}
],
}]
@@ -0,0 +1,36 @@
from __future__ import with_statement
from couchpotato.core.helpers.encoding import toSafeString
from couchpotato.core.logger import CPLog
from couchpotato.core.downloaders.base import Downloader
import os
import urllib
log = CPLog(__name__)
class Blackhole(Downloader):
type = ['nzb', 'torrent']
def download(self, data = {}):
if self.isDisabled() or not self.isCorrectType(data.get('type')):
return
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.' % data.get('type'))
else:
fullPath = os.path.join(directory, toSafeString(data.get('name')) + '.' + data)
if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.' % (data.get('type'), fullPath))
file = urllib.urlopen(data.get('url')).read()
with open(fullPath, 'wb') as f:
f.write(file)
return True
else:
log.error('File %s already exists.' % fullPath)
return False
@@ -0,0 +1,39 @@
from .main import Sabnzbd
def start():
return Sabnzbd()
config = [{
'name': 'sabnzbd',
'groups': [
{
'tab': 'downloaders',
'name': 'sabnzbd',
'label': 'Sabnzbd',
'description': 'Fill in your Sabnzbd settings.',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'bool',
'label': 'Enabled',
'description': 'Send snatched NZBs to Sabnzbd',
},
{
'name': 'host',
'default': 'localhost:8080',
'type': 'string',
'label': 'Host',
'description': 'Test',
},
{
'name': 'api_key',
'default': '',
'type': 'string',
'label': 'Api Key',
'description': 'Used for all calls to Sabnzbd.',
},
],
}
],
}]
@@ -0,0 +1,120 @@
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from tempfile import mkstemp
from urllib import urlencode
import base64
import os
import re
import urllib2
log = CPLog(__name__)
class Sabnzbd(Downloader):
type = ['nzb']
def download(self, data = {}):
if self.isDisabled() or not self.isCorrectType(data.get('type')):
return
log.info("Sending '%s' to SABnzbd." % data.get('name'))
if self.conf('ppDir') and data.get('imdb_id'):
try:
pp_script_fn = self.buildPp(data.get('imdb_id'))
except:
log.info("Failed to create post-processing script.")
pp_script_fn = False
if not pp_script_fn:
pp = False
else:
pp = True
else:
pp = False
params = {
'apikey': self.conf('apikey'),
'cat': self.conf('category'),
'mode': 'addurl',
'name': data.get('url')
}
# sabNzbd complains about "invalid archive file" for newzbin urls
# added using addurl, works fine with addid
if data.get('addbyid'):
params['mode'] = 'addid'
if pp:
params['script'] = pp_script_fn
url = cleanHost(self.conf('host')) + "api?" + urlencode(params)
log.info("URL: " + url)
try:
r = urllib2.urlopen(url, timeout = 30)
except:
log.error("Unable to connect to SAB.")
return False
result = r.read().strip()
if not result:
log.error("SABnzbd didn't return anything.")
return False
log.debug("Result text from SAB: " + result)
if result == "ok":
log.info("NZB sent to SAB successfully.")
return True
elif result == "Missing authentication":
log.error("Incorrect username/password.")
return False
else:
log.error("Unknown error: " + result)
return False
def buildPp(self, imdb_id):
pp_script_path = self.getPpFile()
scriptB64 = '''IyEvdXNyL2Jpbi9weXRob24KaW1wb3J0IG9zCmltcG9ydCBzeXMKcHJpbnQgIkNyZWF0aW5nIGNwLmNw
bmZvIGZvciAlcyIgJSBzeXMuYXJndlsxXQppbWRiSWQgPSB7W0lNREJJREhFUkVdfQpwYXRoID0gb3Mu
cGF0aC5qb2luKHN5cy5hcmd2WzFdLCAiY3AuY3BuZm8iKQp0cnk6CiBmID0gb3BlbihwYXRoLCAndycp
CmV4Y2VwdCBJT0Vycm9yOgogcHJpbnQgIlVuYWJsZSB0byBvcGVuICVzIGZvciB3cml0aW5nIiAlIHBh
dGgKIHN5cy5leGl0KDEpCnRyeToKIGYud3JpdGUob3MucGF0aC5iYXNlbmFtZShzeXMuYXJndlswXSkr
IlxuIitpbWRiSWQpCmV4Y2VwdDoKIHByaW50ICJVbmFibGUgdG8gd3JpdGUgdG8gZmlsZTogJXMiICUg
cGF0aAogc3lzLmV4aXQoMikKZi5jbG9zZSgpCnByaW50ICJXcm90ZSBpbWRiIGlkLCAlcywgdG8gZmls
ZTogJXMiICUgKGltZGJJZCwgcGF0aCkK'''
script = re.sub(r"\{\[IMDBIDHERE\]\}", "'%s'" % imdb_id, base64.b64decode(scriptB64))
try:
f = open(pp_script_path, 'wb')
except:
log.info("Unable to open post-processing script for writing. Check permissions: %s" % pp_script_path)
return False
try:
f.write(script)
f.close()
except:
log.info("Unable to write to post-processing script. Check permissions: %s" % pp_script_path)
return False
log.info("Wrote post-processing script to: %s" % pp_script_path)
return os.path.basename(pp_script_path)
def getPpFile(self):
pp_script_handle, pp_script_path = mkstemp(suffix = '.py', dir = self.conf('ppDir'))
pp_sh = os.fdopen(pp_script_handle)
pp_sh.close()
try:
os.chmod(pp_script_path, int('777', 8))
except:
log.info("Unable to set post-processing script permissions to 777 (may still work correctly): %s" % pp_script_path)
return pp_script_path
+43 -9
View File
@@ -1,4 +1,5 @@
from axl.axel import Event
from couchpotato.core.helpers.variable import merge_dicts
from couchpotato.core.logger import CPLog
import traceback
@@ -19,31 +20,64 @@ def removeEvent(name, handler):
e -= handler
def fireEvent(name, *args, **kwargs):
log.debug('Firing "%s": %s, %s' % (name, args, kwargs))
try:
# Return single handler
single = False
try:
del kwargs['single']
single = True
except: pass
# Merge items
merge = False
try:
del kwargs['merge']
merge = True
except: pass
e = events[name]
e.asynchronous = False
result = e(*args, **kwargs)
results = []
for r in result:
if r[0] == True:
results.append(r[1])
else:
etype, value, tb = r[1]
log.debug(''.join(traceback.format_exception(etype, value, tb)))
if single and not merge:
results = result[0][1]
else:
results = []
for r in result:
if r[0] == True:
results.append(r[1])
else:
errorHandler(r[1])
# Merge the results
if merge:
merged = {}
for result in results:
merged = merge_dicts(merged, result)
results = merged
return results
except Exception, e:
log.debug(e)
log.error('%s: %s' % (name, e))
def fireEventAsync(name, *args, **kwargs):
log.debug('Async "%s": %s, %s' % (name, args, kwargs))
try:
e = events[name]
e.asynchronous = True
e.error_handler = errorHandler
e(*args, **kwargs)
return True
except Exception, e:
log.debug(e)
log.error('%s: %s' % (name, e))
def errorHandler(error):
etype, value, tb = error
log.error(''.join(traceback.format_exception(etype, value, tb)))
def getEvent(name):
return events[name]
+3
View File
@@ -5,16 +5,19 @@ import unicodedata
log = CPLog(__name__)
def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
return ''.join(c for c in cleanedFilename if c in valid_chars)
def simplifyString(original):
string = toSafeString(original)
split = re.split('\W+', string.lower())
return toUnicode(' '.join(split))
def toUnicode(original, *args):
try:
if type(original) is unicode:
+1 -1
View File
@@ -42,7 +42,7 @@ def dictToList(params):
new = {}
for x, value in params.iteritems():
try:
new_value = [dictToList(value2) for value2 in value.itervalues()]
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys())]
except:
new_value = value
+38
View File
@@ -0,0 +1,38 @@
import hashlib
import os.path
def is_dict(object):
return isinstance(object, dict)
def merge_dicts(a, b):
assert is_dict(a), is_dict(b)
dst = a.copy()
stack = [(dst, b)]
while stack:
current_dst, current_src = stack.pop()
for key in current_src:
if key not in current_dst:
current_dst[key] = current_src[key]
else:
if is_dict(current_src[key]) and is_dict(current_dst[key]) :
stack.append((current_dst[key], current_src[key]))
else:
current_dst[key] = current_src[key]
return dst
def md5(text):
return hashlib.md5(text).hexdigest()
def getExt(filename):
return os.path.splitext(filename)[1][1:]
def cleanHost(host):
if not host.startswith(('http://', 'https://')):
host = 'http://' + host
if not host.endswith('/'):
host += '/'
return host
+14 -9
View File
@@ -1,4 +1,4 @@
from couchpotato.core.event import fireEvent, fireEventAsync
from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog
import glob
import os
@@ -17,6 +17,8 @@ class Loader:
self.paths = {
'plugin' : ('couchpotato.core.plugins', os.path.join(root, 'couchpotato', 'core', 'plugins')),
'provider' : ('couchpotato.core.providers', os.path.join(root, 'couchpotato', 'core', 'providers')),
'notifications' : ('couchpotato.core.notifications', os.path.join(root, 'couchpotato', 'core', 'notifications')),
'downloaders' : ('couchpotato.core.downloaders', os.path.join(root, 'couchpotato', 'core', 'downloaders')),
}
for type, tuple in self.paths.iteritems():
@@ -28,14 +30,17 @@ class Loader:
for module_name, plugin in sorted(self.modules.iteritems()):
# Load module
m = getattr(self.loadModule(module_name), plugin.get('name'))
try:
m = getattr(self.loadModule(module_name), plugin.get('name'))
log.info("Loading '%s'" % module_name)
log.info("Loading %s: %s" % (plugin['type'], plugin['name']))
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
# Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False)
self.loadPlugins(m, plugin.get('name'))
self.loadPlugins(m, plugin.get('name'))
except Exception, e:
log.error(e)
if did_save:
fireEvent('settings.save')
@@ -51,12 +56,12 @@ class Loader:
def loadSettings(self, module, name, save = True):
try:
for section in module.config:
fireEventAsync('settings.options', section['name'], section)
fireEvent('settings.options', section['name'], section)
options = {}
for group in section['groups']:
for option in group['options']:
options[option['name']] = option['default']
fireEventAsync('settings.register', section_name = section['name'], options = options, save = save)
fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True
except Exception, e:
log.debug("Failed loading settings for '%s': %s" % (name, e))
@@ -67,7 +72,7 @@ class Loader:
module.start()
return True
except Exception, e:
log.debug("Failed loading plugin '%s': %s" % (name, e))
log.error("Failed loading plugin '%s': %s" % (name, e))
return False
def addModule(self, type, module, name):
Whitespace-only changes.
+29
View File
@@ -0,0 +1,29 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
class Notification(Plugin):
default_title = 'CouchPotato'
test_message = 'ZOMG Lazors Pewpewpew!'
def __init__(self):
addEvent('notify', self.notify)
def notify(self, message = '', data = {}):
pass
def conf(self, attr):
return Env.setting(attr, self.__class__.__name__.lower())
def isDisabled(self):
return not self.isEnabled()
def isEnabled(self):
return self.conf('enabled', True)
def test(self):
success = self.notify(message = self.test_message)
return jsonified({'success': success})
@@ -0,0 +1,33 @@
from .main import Growl
def start():
return Growl()
config = [{
'name': 'growl',
'groups': [
{
'tab': 'notifications',
'name': 'growl',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'enabler',
'description': '',
},
{
'name': 'host',
'default': 'localhost',
'description': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
'description': '',
},
],
}
],
}]
@@ -0,0 +1,111 @@
# Based on netprowl by the following authors.
# Altered 1st October 2010 - Tim Child.
# Have added the ability for the command line arguments to take a password.
# Altered 1-17-2010 - Tanner Stokes - www.tannr.com
# Added support for command line arguments
# ORIGINAL CREDITS
# """Growl 0.6 Network Protocol Client for Python"""
# __version__ = "0.6.3"
# __author__ = "Rui Carmo (http://the.taoofmac.com)"
# __copyright__ = "(C) 2004 Rui Carmo. Code under BSD License."
# __contributors__ = "Ingmar J Stein (Growl Team), John Morrissey (hashlib patch)"
import struct
try:
import hashlib
md5_constructor = hashlib.md5
except ImportError:
import md5
md5_constructor = md5.new
GROWL_UDP_PORT = 9887
GROWL_PROTOCOL_VERSION = 1
GROWL_TYPE_REGISTRATION = 0
GROWL_TYPE_NOTIFICATION = 1
class GrowlRegistrationPacket:
"""Builds a Growl Network Registration packet.
Defaults to emulating the command-line growlnotify utility."""
def __init__(self, application = "CouchPotato", password = None):
self.notifications = []
self.defaults = [] # array of indexes into notifications
self.application = application.encode("utf-8")
self.password = password
def addNotification(self, notification = "General Notification", enabled = True):
"""Adds a notification type and sets whether it is enabled on the GUI"""
self.notifications.append(notification)
if enabled:
self.defaults.append(len(self.notifications) - 1)
def payload(self):
"""Returns the packet payload."""
self.data = struct.pack("!BBH",
GROWL_PROTOCOL_VERSION,
GROWL_TYPE_REGISTRATION,
len(self.application)
)
self.data += struct.pack("BB",
len(self.notifications),
len(self.defaults)
)
self.data += self.application
for notification in self.notifications:
encoded = notification.encode("utf-8")
self.data += struct.pack("!H", len(encoded))
self.data += encoded
for default in self.defaults:
self.data += struct.pack("B", default)
self.checksum = md5()
self.checksum.update(self.data)
if self.password:
self.checksum.update(self.password)
self.data += self.checksum.digest()
return self.data
class GrowlNotificationPacket:
"""Builds a Growl Network Notification packet.
Defaults to emulating the command-line growlnotify utility."""
def __init__(self, application = "CouchPotato",
notification = "General Notification", title = "Title",
description = "Description", priority = 0, sticky = False, password = None):
self.application = application.encode("utf-8")
self.notification = notification.encode("utf-8")
self.title = title.encode("utf-8")
self.description = description.encode("utf-8")
flags = (priority & 0x07) * 2
if priority < 0:
flags |= 0x08
if sticky:
flags = flags | 0x0100
self.data = struct.pack("!BBHHHHH",
GROWL_PROTOCOL_VERSION,
GROWL_TYPE_NOTIFICATION,
flags,
len(self.notification),
len(self.title),
len(self.description),
len(self.application)
)
self.data += self.notification
self.data += self.title
self.data += self.description
self.data += self.application
self.checksum = md5_constructor()
self.checksum.update(self.data)
if password:
self.checksum.update(password)
self.data += self.checksum.digest()
def payload(self):
"""Returns the packet payload."""
return self.data
@@ -0,0 +1,45 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.notifications.growl.growl import GROWL_UDP_PORT, \
GrowlRegistrationPacket, GrowlNotificationPacket
from couchpotato.environment import Env
from socket import AF_INET, SOCK_DGRAM, socket
log = CPLog(__name__)
class Growl(Notification):
def __init__(self):
addEvent('notify', self.notify)
addEvent('notify.growl', self.notify)
addApiView('notify.growl.test', self.test)
def conf(self, attr):
return Env.setting(attr, 'growl')
def notify(self, message = '', data = {}):
if self.isDisabled():
return
hosts = [x.strip() for x in self.conf('host').split(",")]
password = self.conf('password')
for curHost in hosts:
addr = (curHost, GROWL_UDP_PORT)
s = socket(AF_INET, SOCK_DGRAM)
p = GrowlRegistrationPacket(password = password)
p.addNotification()
s.sendto(p.payload(), addr)
# send notification
p = GrowlNotificationPacket(title = self.default_title, description = message, priority = 0, sticky = False, password = password)
s.sendto(p.payload(), addr)
s.close()
log.info('Growl notifications sent.')
@@ -0,0 +1,37 @@
from .main import NMJ
def start():
return NMJ()
config = [{
'name': 'nmj',
'groups': [
{
'tab': 'notifications',
'name': 'nmj',
'label': 'NMJ',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'host',
'default': 'localhost',
'description': '',
},
{
'name': 'database',
'default': '',
'description': '',
},
{
'name': 'mount',
'default': '',
'description': '',
},
],
}
],
}]
+131
View File
@@ -0,0 +1,131 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
import re
import telnetlib
import urllib
import urllib2
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
log = CPLog(__name__)
class NMJ(Notification):
def __init__(self):
addEvent('notify', self.notify)
addEvent('notify.nmj', self.notify)
addApiView('notify.nmj.test', self.test)
addApiView('notify.nmj.auto_config', self.autoConfig)
def conf(self, attr):
return Env.setting(attr, 'nmj')
def autoConfig(self):
params = getParams()
host = params.get('host', 'localhost')
database = ''
mount = ''
try:
terminal = telnetlib.Telnet(host)
except Exception:
log.error('Warning: unable to get a telnet session to %s' % (host))
return self.failed()
log.debug('Connected to %s via telnet' % (host))
terminal.read_until('sh-3.00# ')
terminal.write('cat /tmp/source\n')
terminal.write('cat /tmp/netshare\n')
terminal.write('exit\n')
tnoutput = terminal.read_all()
match = re.search(r'(.+\.db)\r\n?(.+)(?=sh-3.00# cat /tmp/netshare)', tnoutput)
if match:
database = match.group(1)
device = match.group(2)
log.info('Found NMJ database %s on device %s' % (database, device))
else:
log.error('Could not get current NMJ database on %s, NMJ is probably not running!' % (host))
return self.failed()
if device.startswith('NETWORK_SHARE/'):
match = re.search('.*(?=\r\n?%s)' % (re.escape(device[14:])), tnoutput)
if match:
mount = match.group().replace('127.0.0.1', host)
log.info('Found mounting url on the Popcorn Hour in configuration: %s' % (mount))
else:
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed()
return jsonified({
'success': True,
'database': database,
'mount': mount,
})
def notify(self, message = '', data = {}):
if self.isDisabled():
return False
host = self.conf('host')
mount = self.conf('mount')
database = self.conf('database')
if self.mount:
try:
req = urllib2.Request(mount)
log.debug('Try to mount network drive via url: %s' % (mount))
handle = urllib2.urlopen(req)
except IOError, e:
log.error('Warning: Couldn\'t contact popcorn hour on host %s: %s' % (host, e))
return False
params = {
'arg0': 'scanner_start',
'arg1': database,
'arg2': 'background',
'arg3': '',
}
params = urllib.urlencode(params)
UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s'
updateUrl = UPDATE_URL % {'host': host, 'params': params}
try:
req = urllib2.Request(updateUrl)
log.debug('Sending NMJ scan update command via url: %s' % (updateUrl))
handle = urllib2.urlopen(req)
response = handle.read()
except IOError, e:
log.error('Warning: Couldn\'t contact Popcorn Hour on host %s: %s' % (host, e))
return False
try:
et = etree.fromstring(response)
result = et.findtext('returnValue')
except SyntaxError, e:
log.error('Unable to parse XML returned from the Popcorn Hour: %s' % (e))
return False
if int(result) > 0:
log.error('Popcorn Hour returned an errorcode: %s' % (result))
return False
else:
log.info('NMJ started background scan')
return True
def failed(self):
return jsonified({'success': False})
@@ -0,0 +1,32 @@
from .main import Notifo
def start():
return Notifo()
config = [{
'name': 'notifo',
'groups': [
{
'tab': 'notifications',
'name': 'notifo',
'description': '',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'username',
'default': '',
'type': 'string',
},
{
'name': 'password',
'default': '',
'type': 'password',
},
],
}
],
}]
@@ -0,0 +1,53 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env
from flask.helpers import json
import base64
import urllib
import urllib2
log = CPLog(__name__)
class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification'
def __init__(self):
addEvent('notify', self.notify)
addEvent('notify.notifo', self.notify)
addApiView('notify.notifo.test', self.test)
def conf(self, attr):
return Env.setting(attr, 'notifo')
def notify(self, message = '', data = {}):
if self.isDisabled():
return False
try:
data = urllib.urlencode({
'msg': toUnicode(message),
})
req = urllib2.Request(self.url)
authHeader = "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('api_key')))[:-1]
req.add_header("Authorization", authHeader)
handle = urllib2.urlopen(req, data)
result = json.load(handle)
if result['status'] != 'success' or result['response_message'] != 'OK':
raise Exception
except Exception, e:
log.error('Notification failed: %s' % e)
return False
log.info('Notifo notification successful.')
return True
@@ -0,0 +1,33 @@
from .main import Plex
def start():
return Plex()
config = [{
'name': 'plex',
'groups': [
{
'tab': 'notifications',
'name': 'plex',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'enabler',
'description': '',
},
{
'name': 'host',
'default': 'localhost',
'description': '',
},
{
'name': 'password',
'default': '',
'type': 'password',
'description': '',
},
],
}
],
}]
@@ -0,0 +1,43 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from xml.dom import minidom
import urllib
log = CPLog(__name__)
class Plex(Notification):
def __init__(self):
addEvent('notify', self.notify)
addEvent('notify.plex', self.notify)
addApiView('notify.plex.test', self.test)
def notify(self, message = '', data = {}):
if self.isDisabled():
return
log.info('Sending notification to Plex')
hosts = [x.strip() for x in self.conf('host').split(",")]
for host in hosts:
source_type = ['movie']
base_url = 'http://%s/library/sections' % host
refresh_url = '%s/%%s/refresh' % base_url
try:
xml_sections = minidom.parse(urllib.urlopen(base_url))
sections = xml_sections.getElementsByTagName('Directory')
for s in sections:
if s.getAttribute('type') in source_type:
url = refresh_url % s.getAttribute('key')
x = urllib.urlopen(url)
except:
log.error('Plex library update failed for %s.' % host)
return True
@@ -0,0 +1,35 @@
from .main import Prowl
def start():
return Prowl()
config = [{
'name': 'prowl',
'groups': [
{
'tab': 'notifications',
'name': 'prowl',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'enabler',
'description': '',
},
{
'name': 'api_key',
'default': '',
'label': 'Api key',
'description': '',
},
{
'name': 'priority',
'default': '0',
'type': 'dropdown',
'description': '',
'values': [('Very Low', -2), ('Moderate', -1), ('Normal', 0), ('High', 1), ('Emergency', 2)]
},
],
}
],
}]
@@ -0,0 +1,51 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
from urllib import urlencode
log = CPLog(__name__)
class Prowl(Notification):
def __init__(self):
addEvent('notify', self.notify)
addEvent('notify.prowl', self.notify)
addApiView('notify.prowl.test', self.test)
def notify(self, message = '', data = {}):
if self.isDisabled():
return
http_handler = HTTPSConnection('api.prowlapp.com')
data = {
'apikey': self.conf('api_key'),
'application': self.default_title,
'event': self.default_title,
'description': toUnicode(message),
'priority': self.conf('priority'),
}
http_handler.request('POST',
'/publicapi/add',
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = urlencode(data)
)
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
log.info('Prowl notifications sent.')
return True
elif request_status == 401:
log.error('Prowl auth failed: %s' % response.reason)
return False
else:
log.error('Prowl notification failed.')
return False
@@ -0,0 +1,38 @@
from .main import XBMC
def start():
return XBMC()
config = [{
'name': 'xbmc',
'groups': [
{
'tab': 'notifications',
'name': 'xbmc',
'options': [
{
'name': 'enabled',
'default': False,
'type': 'enabler',
'description': '',
},
{
'name': 'host',
'default': 'localhost:8080',
'description': '',
},
{
'name': 'username',
'default': 'xbmc',
'description': '',
},
{
'name': 'password',
'default': 'xbmc',
'type': 'password',
'description': '',
},
],
}
],
}]
@@ -0,0 +1,47 @@
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import base64
import urllib
import urllib2
log = CPLog(__name__)
class XBMC(Notification):
def __init__(self):
addEvent('notify', self.notify)
addEvent('notify.xbmc', self.notify)
addApiView('notify.xbmc.test', self.test)
def notify(self, message = '', data = {}):
if self.isDisabled():
return
for host in [x.strip() for x in self.conf('host').split(",")]:
self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host)
self.send({'command': 'ExecBuiltIn', 'parameter': 'XBMC.updatelibrary(video)'}, host)
return True
def send(self, command, host):
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, urllib.urlencode(command))
try:
req = urllib2.Request(url)
if self.password:
authHeader = "Basic %s" % base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password')))[:-1]
req.add_header("Authorization", authHeader)
urllib2.urlopen(req, timeout = 10).read()
except Exception, e:
log.error("Couldn't sent command to XBMC. %s" % e)
return False
log.info('XBMC notification to %s successful.' % host)
return True
@@ -0,0 +1,6 @@
from .main import FileManager
def start():
return FileManager()
config = []
+103
View File
@@ -0,0 +1,103 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import md5, getExt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env
from flask.helpers import send_from_directory
import os.path
import urllib2
log = CPLog(__name__)
class FileManager(Plugin):
def __init__(self):
addEvent('file.add', self.add)
addEvent('file.download', self.download)
addEvent('file.types', self.getTypes)
addApiView('file.cache/<path:file>', self.showImage)
def showImage(self, file = ''):
cache_dir = Env.get('cache_dir')
filename = file.replace(cache_dir[1:] + '/', '')
return send_from_directory(cache_dir, filename)
def download(self, url = '', dest = None, overwrite = False):
try:
file = urllib2.urlopen(url)
if not dest: # to Cache
dest = os.path.join(Env.get('cache_dir'), '%s.%s' % (md5(url), getExt(url)))
if overwrite or not os.path.exists(dest):
log.debug('Writing file to: %s' % dest)
output = open(dest, 'wb')
output.write(file.read())
output.close()
else:
log.debug('File already exists: %s' % dest)
return dest
except Exception, e:
log.error('Unable to download file "%s": %s' % (url, e))
return False
def add(self, path = '', part = 1, type = (), properties = {}):
db = get_session()
f = db.query(File).filter_by(path = path).first()
if not f:
f = File()
db.add(f)
f.path = path
f.part = part
f.type_id = self.getType(type).id
db.commit()
db.expunge(f)
return f
def getType(self, type):
db = get_session()
type, identifier = type
ft = db.query(FileType).filter_by(identifier = identifier).first()
if not ft:
ft = FileType(
type = type,
identifier = identifier,
name = identifier[0].capitalize() + identifier[1:]
)
db.add(ft)
db.commit()
return ft
def getTypes(self):
db = get_session()
results = db.query(FileType).all()
types = []
for type in results:
temp = type.to_dict()
types.append(temp)
return types
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.file_browser.main import FileBrowser
from .main import FileBrowser
def start():
return FileBrowser()
+1 -1
View File
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.library.main import LibraryPlugin
from .main import LibraryPlugin
def start():
return LibraryPlugin()
+57 -8
View File
@@ -1,32 +1,81 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library
from couchpotato.core.settings.model import Library, LibraryTitle
log = CPLog(__name__)
class LibraryPlugin(Plugin):
def __init__(self):
addEvent('library.add', self.add)
addEvent('library.update', self.update)
def add(self, attrs = {}):
db = get_session();
db = get_session()
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
if not l:
l = Library(
name = attrs.get('name'),
year = attrs.get('year'),
identifier = attrs.get('identifier'),
description = attrs.get('description')
plot = attrs.get('plot'),
tagline = attrs.get('tagline')
)
title = LibraryTitle(
title = attrs.get('title')
)
l.titles.append(title)
db.add(l)
db.commit()
# Update library info
fireEventAsync('library.update', library = l, default_title = attrs.get('title', ''))
#db.remove()
return l
def update(self, item):
def update(self, library, default_title = ''):
pass
db = get_session()
library = db.query(Library).filter_by(identifier = library.identifier).first()
info = fireEvent('provider.movie.info', merge = True, identifier = library.identifier)
# Main info
library.plot = info.get('plot', '')
library.tagline = info.get('tagline', '')
library.year = info.get('year', 0)
# Titles
[db.delete(title) for title in library.titles]
titles = info.get('titles')
log.debug('Adding titles: %s' % titles)
for title in titles:
t = LibraryTitle(
title = title,
default = title.lower() == default_title.lower()
)
library.titles.append(t)
db.commit()
# Files
images = info.get('images')
for type in images:
for image in images[type]:
file_path = fireEvent('file.download', url = image, single = True)
file = fireEvent('file.add', path = file_path, type = ('image', type[:-1]), single = True)
try:
library.files.append(file)
db.commit()
except:
log.debug('File already attached to library')
fireEvent('library.update.after')
+1 -1
View File
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.movie.main import MoviePlugin
from .main import MoviePlugin
def start():
return MoviePlugin()
+71 -32
View File
@@ -1,9 +1,9 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent
from couchpotato.core.event import fireEvent, fireEventAsync
from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, Profile
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
from urllib import urlencode
@@ -13,30 +13,28 @@ class MoviePlugin(Plugin):
def __init__(self):
addApiView('movie.search', self.search)
addApiView('movie.list', self.list)
addApiView('movie.refresh', self.refresh)
addApiView('movie.add', self.add)
addApiView('movie.edit', self.edit)
addApiView('movie.delete', self.delete)
def list(self):
a = getParams()
params = getParams()
db = get_session()
results = get_session().query(Movie).filter(
Movie.releases.any(
Release.status.has(identifier = 'wanted')
)
).all()
results = db.query(Movie).filter(
Movie.status.has(identifier = params.get('status', 'active'))
).all()
movies = []
for movie in results:
temp = {
'id': movie.id,
'name': movie.id,
'releases': [],
}
for release in movie.releases:
temp['releases'].append({
'status': release.status.label,
'quality': release.quality.label
})
temp = movie.to_dict(deep = {
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {}
})
movies.append(temp)
@@ -46,22 +44,41 @@ class MoviePlugin(Plugin):
'movies': movies,
})
def refresh(self):
params = getParams()
db = get_session()
movie = db.query(Movie).filter_by(id = params.get('id')).first()
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
if movie:
#addEvent('library.update.after', )
fireEventAsync('library.update', library = movie.library, default_title = default_title)
return jsonified({
'success': True,
})
def search(self):
a = getParams()
cache_key = '%s/%s' % (__name__, urlencode(a))
params = getParams()
cache_key = '%s/%s' % (__name__, urlencode(params))
movies = Env.get('cache').get(cache_key)
if not movies:
results = fireEvent('provider.movie.search', q = a.get('q'))
results = fireEvent('provider.movie.search', q = params.get('q'))
# Combine movie results
movies = []
for r in results:
movies += r
Env.get('cache').set(cache_key, movies, timeout = 10)
Env.get('cache').set(cache_key, movies)
return jsonified({
'success': True,
@@ -71,24 +88,46 @@ class MoviePlugin(Plugin):
def add(self):
a = getParams()
params = getParams()
db = get_session();
library = fireEvent('library.add', attrs = a)
profile = db.query(Profile).filter_by(identifier = a.get('profile_identifier'))
m = db.query(Movie).filter_by(library = library).first()
library = fireEvent('library.add', single = True, attrs = params)
status = fireEvent('status.add', 'active', single = True)
m = db.query(Movie).filter_by(library_id = library.id).first()
if not m:
m = Movie(
library = library,
profile = profile,
library_id = library.id,
profile_id = params.get('profile_id')
)
db.add(m)
db.commit()
m.status_id = status.id
db.commit()
return jsonified({
'success': True,
'added': True,
'params': a,
'movie': m.to_dict(deep = {
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}}
})
})
def edit(self):
pass
def delete(self):
params = getParams()
db = get_session()
status = fireEvent('status.add', 'deleted', single = True)
movie = db.query(Movie).filter_by(id = params.get('id')).first()
movie.status_id = status.id
db.commit()
return jsonified({
'success': True,
})
+1 -1
View File
@@ -1,4 +1,4 @@
from couchpotato.core.plugins.profile.main import ProfilePlugin
from .main import ProfilePlugin
def start():
return ProfilePlugin()
+69 -7
View File
@@ -1,28 +1,90 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType
log = CPLog(__name__)
class ProfilePlugin(Plugin):
def __init__(self):
addEvent('profile.get', self.get)
addEvent('profile.all', self.all)
addApiView('profile.save', self.save)
addApiView('profile.delete', self.delete)
def get(self, key = ''):
def all(self):
pass
db = get_session()
profiles = db.query(Profile).all()
temp = []
for profile in profiles:
temp.append(profile.to_dict(deep = {'types': {}}))
return temp
def save(self):
a = getParams()
params = getParams()
db = get_session()
p = db.query(Profile).filter_by(id = params.get('id')).first()
if not p:
p = Profile()
db.add(p)
p.label = params.get('label')
p.order = params.get('order', p.order if p.order else 0)
p.core = params.get('core', False)
#delete old types
[db.delete(t) for t in p.types]
order = 0
for type in params.get('types', []):
t = ProfileType(
order = order,
finish = type.get('finish'),
wait_for = params.get('wait_for'),
quality_id = type.get('quality_id')
)
p.types.append(t)
order += 1
db.commit()
return jsonified({
'success': True,
'a': a
'profile': p.to_dict(deep = {'types': {}})
})
def delete(self):
pass
id = getParam('id')
db = get_session()
success = False
message = ''
try:
p = db.query(Profile).filter_by(id = id).first()
db.delete(p)
db.commit()
success = True
except Exception, e:
message = 'Failed deleting Profile: %s' % e
log.error(message)
return jsonified({
'success': success,
'message': message
})
@@ -0,0 +1,6 @@
from .main import QualityPlugin
def start():
return QualityPlugin()
config = []
+97
View File
@@ -0,0 +1,97 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import Quality, Profile, ProfileType
log = CPLog(__name__)
class QualityPlugin:
qualities = [
{'identifier': 'bd50', 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['1080p', 'bd25'], 'allow': [], 'ext':[], 'tags': ['x264', 'h264', 'blu ray']},
{'identifier': '1080p', 'size': (5000, 20000), 'label': '1080P', 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': '720p', 'size': (3500, 10000), 'label': '720P', 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['x264', 'h264', 'bluray']},
{'identifier': 'brrip', 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['mkv', 'avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['dvdscr'], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
def __init__(self):
addEvent('quality.all', self.all)
addEvent('app.load', self.fill)
def all(self):
db = get_session()
qualities = db.query(Quality).all()
temp = []
for quality in qualities:
q = dict(self.getQuality(quality.identifier), **quality.to_dict())
temp.append(q)
return temp
def getQuality(self, identifier):
for q in self.qualities:
if identifier == q.get('identifier'):
return q
def fill(self):
db = get_session();
order = 0
for q in self.qualities:
# Create quality
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not quality:
log.info('Creating quality: %s' % q.get('label'))
quality = Quality()
db.add(quality)
quality.order = order
quality.identifier = q.get('identifier')
quality.label = q.get('label')
quality.size_min, quality.size_max = q.get('size')
# Create single quality profile
profile = db.query(Profile).filter(
Profile.core == True
).filter(
Profile.types.any(quality = quality)
).all()
if not profile:
log.info('Creating profile: %s' % q.get('label'))
profile = Profile(
core = True,
label = toUnicode(quality.label),
order = order
)
db.add(profile)
profile_type = ProfileType(
quality = quality,
profile = profile,
finish = True,
order = 0
)
profile.types.append(profile_type)
order += 1
db.commit()
return True
@@ -0,0 +1,6 @@
from .main import StatusPlugin
def start():
return StatusPlugin()
config = []
+68
View File
@@ -0,0 +1,68 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.settings.model import Status
log = CPLog(__name__)
class StatusPlugin:
statuses = {
'active': 'Active',
'done': 'Done',
'downloaded': 'Downloaded',
'wanted': 'Wanted',
'deleted': 'Deleted',
}
def __init__(self):
addEvent('status.add', self.add)
addEvent('status.all', self.all)
addEvent('app.load', self.fill)
def all(self):
db = get_session()
statuses = db.query(Status).all()
temp = []
for status in statuses:
s = status.to_dict()
temp.append(s)
return temp
def add(self, identifier):
db = get_session()
s = db.query(Status).filter_by(identifier = identifier).first()
if not s:
s = Status(
identifier = identifier,
label = identifier.capitalize()
)
db.add(s)
db.commit()
#db.remove()
return s
def fill(self):
db = get_session()
for identifier, label in self.statuses.iteritems():
s = db.query(Status).filter_by(identifier = identifier).first()
if not s:
log.info('Creating status: %s' % label)
s = Status(
identifier = identifier,
label = toUnicode(label)
)
db.add(s)
s.label = label
db.commit()
+2 -1
View File
@@ -1,8 +1,9 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
log = CPLog(__name__)
class Provider():
class Provider(Plugin):
type = None # movie, nzb, torrent, subtitle, trailer
timeout = 10 # Default timeout for url requests
@@ -0,0 +1,116 @@
from __future__ import with_statement
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
from libs.themoviedb import tmdb
import copy
log = CPLog(__name__)
class TMDBWrapper(Provider):
"""Api for theMovieDb"""
type = 'movie'
apiUrl = 'http://api.themoviedb.org/2.1'
imageUrl = 'http://hwcdn.themoviedb.org'
def __init__(self):
addEvent('provider.movie.search', self.search)
addEvent('provider.movie.info', self.getInfo)
# Use base wrapper
tmdb.Config.api_key = self.conf('api_key')
def conf(self, attr):
return Env.setting(attr, 'themoviedb')
def search(self, q, limit = 12):
''' Find movie by name '''
if self.isDisabled():
return False
log.debug('TheMovieDB - Searching for movie: %s' % q)
raw = tmdb.search(simplifyString(q))
results = []
if raw:
try:
nr = 0
for movie in raw:
results.append(self.parseMovie(movie))
nr += 1
if nr == limit:
break
log.info('TheMovieDB - Found: %s' % [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
return results
except SyntaxError, e:
log.error('Failed to parse XML response: %s' % e)
return False
return results
def getInfo(self, identifier = None):
result = {}
movie = tmdb.imdbLookup(id = identifier)[0]
if movie:
result = self.parseMovie(movie)
return result
def parseMovie(self, movie):
year = str(movie.get('released', 'none'))[:4]
# Poster url
poster = self.getImage(movie, type = 'poster')
backdrop = self.getImage(movie, type = 'backdrop')
# 1900 is the same as None
if year == '1900' or year.lower() == 'none':
year = None
movie_data = {
'id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
'images': {
'posters': [poster],
'backdrops': [backdrop],
},
'imdb': movie.get('imdb_id'),
'year': year,
'plot': movie.get('overview', ''),
'tagline': '',
}
# Add alternative names
for alt in ['original_name', 'alternative_name']:
alt_name = toUnicode(movie.get(alt))
if alt_name and not alt_name in movie_data['titles'] and alt_name.lower() != 'none' and alt_name != None:
movie_data['titles'].append(alt_name)
return movie_data
def getImage(self, movie, type = 'poster'):
image = ''
for image in movie.get('images', []):
if(image.get('type') == type):
image = image.get('thumb')
break
return image
def isDisabled(self):
if self.conf('api_key') == '':
log.error('No API key provided.')
True
else:
False
-107
View File
@@ -1,107 +0,0 @@
from __future__ import with_statement
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env
from libs.themoviedb import tmdb
from urllib import quote_plus
import copy
import simplejson as json
import urllib2
log = CPLog(__name__)
class TMDBWrapper(Provider):
"""Api for theMovieDb"""
type = 'movie'
apiUrl = 'http://api.themoviedb.org/2.1'
imageUrl = 'http://hwcdn.themoviedb.org'
def __init__(self):
addEvent('provider.movie.search', self.search)
addEvent('provider.movie.info', self.getInfo)
# Use base wrapper
tmdb.Config.api_key = self.conf('api_key')
def conf(self, attr):
return Env.setting(attr, 'themoviedb')
def search(self, q, limit = 12, alternative = True):
''' Find movie by name '''
if self.isDisabled():
return False
log.debug('TheMovieDB - Searching for movie: %s' % q)
raw = tmdb.search(simplifyString(q))
#url = "%s/%s/%s/json/%s/%s" % (self.apiUrl, 'Movie.search', 'en', self.conf('api_key'), quote_plus(simplifyString(q)))
# data = urllib2.urlopen(url)
# jsn = json.load(data)
if raw:
log.debug('TheMovieDB - Parsing RSS')
try:
results = []
nr = 0
for movie in raw:
for k, x in movie.iteritems():
print k
print x
year = str(movie.get('released', 'none'))[:4]
# Poster url
poster = ''
for p in movie.get('images'):
if(p.get('type') == 'poster'):
poster = p.get('thumb')
break
# 1900 is the same as None
if year == '1900' or year.lower() == 'none':
year = None
movie_data = {
'id': int(movie.get('id', 0)),
'name': toUnicode(movie.get('name')),
'poster': poster,
'imdb': movie.get('imdb_id'),
'year': year,
'tagline': 'This is the tagline of the movie',
}
results.append(copy.deepcopy(movie_data))
alternativeName = movie.get('alternative_name')
if alternativeName and alternative:
if alternativeName.lower() != movie['name'].lower() and alternativeName.lower() != 'none' and alternativeName != None:
movie_data['name'] = toUnicode(alternativeName)
results.append(copy.deepcopy(movie_data))
nr += 1
if nr == limit:
break
log.info('TheMovieDB - Found: %s' % [result['name'] + u' (' + str(result['year']) + ')' for result in results])
return results
except SyntaxError, e:
log.error('TheMovieDB - Failed to parse XML response from TheMovieDb: %s' % e)
return False
return results
def getInfo(self):
pass
def isDisabled(self):
if self.conf('api_key') == '':
log.error('TheMovieDB - No API key provided for TheMovieDB')
True
else:
False
+58 -21
View File
@@ -2,8 +2,10 @@ from elixir.entity import Entity
from elixir.fields import Field
from elixir.options import options_defaults
from elixir.relationships import OneToMany, ManyToOne
from sqlalchemy.types import Integer, String, Unicode, UnicodeText, Boolean, \
Float
from libs.elixir.options import using_options
from libs.elixir.relationships import ManyToMany
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, Float, \
String
options_defaults["shortnames"] = True
@@ -20,24 +22,48 @@ class Movie(Entity):
The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails"""
library = ManyToOne('Library')
last_edit = Field(Integer)
library = ManyToOne('Library')
status = ManyToOne('Status')
profile = ManyToOne('Profile')
releases = OneToMany('Release')
files = OneToMany('File')
files = ManyToMany('File')
class Library(Entity):
""""""
title = Field(Unicode)
year = Field(Integer)
identifier = Field(Unicode)
identifier = Field(String(20))
rating = Field(Float)
plot = Field(UnicodeText)
tagline = Field(UnicodeText(255))
status = ManyToOne('Status')
movie = OneToMany('Movie')
titles = OneToMany('LibraryTitle')
files = ManyToMany('File')
class LibraryTitle(Entity):
""""""
title = Field(Unicode)
default = Field(Boolean)
language = OneToMany('Language')
libraries = ManyToOne('Library')
class Language(Entity):
""""""
identifier = Field(String(20))
label = Field(Unicode)
titles = ManyToOne('LibraryTitle')
class Release(Entity):
@@ -47,7 +73,7 @@ class Release(Entity):
movie = ManyToOne('Movie')
status = ManyToOne('Status')
quality = ManyToOne('Quality')
files = OneToMany('File')
files = ManyToMany('File')
history = OneToMany('History')
@@ -55,41 +81,49 @@ class Status(Entity):
"""The status of a release, such as Downloaded, Deleted, Wanted etc"""
identifier = Field(String(20), unique = True)
label = Field(String(20))
label = Field(Unicode(20))
releases = OneToMany('Release')
movies = OneToMany('Movie')
class Quality(Entity):
"""Quality name of a release, DVD, 720P, DVD-Rip etc"""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(String(20))
label = Field(Unicode(20))
order = Field(Integer)
size_min = Field(Integer)
size_max = Field(Integer)
releases = OneToMany('Release')
profile_types = ManyToOne('ProfileType')
profile_types = OneToMany('ProfileType')
class Profile(Entity):
""""""
using_options(order_by = 'order')
identifier = Field(String(20), unique = True)
label = Field(Unicode(50))
order = Field(Integer)
wait_for = Field(Integer)
core = Field(Boolean)
hide = Field(Boolean)
movie = OneToMany('Movie')
profile_type = OneToMany('ProfileType')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
class ProfileType(Entity):
""""""
using_options(order_by = 'order')
order = Field(Integer)
mark_completed = Field(Boolean)
finish = Field(Boolean)
wait_for = Field(Integer)
type = OneToMany('Quality')
quality = ManyToOne('Quality')
profile = ManyToOne('Profile')
@@ -97,19 +131,22 @@ class File(Entity):
"""File that belongs to a release."""
path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer)
part = Field(Integer, default = 1)
history = OneToMany('RenameHistory')
movie = ManyToOne('Movie')
release = ManyToOne('Release')
type = ManyToOne('FileType')
properties = OneToMany('FileProperty')
history = OneToMany('RenameHistory')
movie = ManyToMany('Movie')
release = ManyToMany('Release')
library = ManyToMany('Library')
class FileType(Entity):
"""Types could be trailer, subtitle, movie, partial movie etc."""
identifier = Field(String(20), unique = True)
type = Field(Unicode(20))
name = Field(Unicode(50), nullable = False)
files = OneToMany('File')
@@ -135,8 +172,8 @@ class History(Entity):
class RenameHistory(Entity):
"""Remembers from where to where files have been moved."""
old = Field(String(255))
new = Field(String(255))
old = Field(Unicode(255))
new = Field(Unicode(255))
file = ManyToOne('File')