69 changed files with 3432 additions and 562 deletions
@ -1,2 +1,3 @@ |
|||
/settings.conf |
|||
/logs/*.log |
|||
/logs/*.log |
|||
/_source/ |
@ -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 |
@ -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 |
@ -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': '', |
|||
}, |
|||
], |
|||
} |
|||
], |
|||
}] |
@ -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 = [] |
@ -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,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 = ''): |
|||
|
|||
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') |
|||
|
|||
pass |
|||
fireEvent('library.update.after') |
|||
|
@ -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): |
|||
|
|||
db = get_session() |
|||
profiles = db.query(Profile).all() |
|||
|
|||
pass |
|||
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 = [] |
@ -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 = [] |
@ -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() |
@ -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 |
@ -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 |
After Width: | Height: | Size: 228 B |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 28 KiB |
After Width: | Height: | Size: 206 B |
After Width: | Height: | Size: 6.4 KiB |
@ -0,0 +1,77 @@ |
|||
var File = new Class({ |
|||
|
|||
initialize: function(file){ |
|||
var self = this; |
|||
|
|||
self.data = file; |
|||
self.type = File.Type.get(file.type_id); |
|||
|
|||
self['create'+(self.type.type).capitalize()]() |
|||
|
|||
}, |
|||
|
|||
createImage: function(){ |
|||
var self = this; |
|||
|
|||
self.el = new Element('div.type_image').adopt( |
|||
new Element('img', { |
|||
'src': Api.createUrl('file.cache') + self.data.path.substring(1) + '/' |
|||
}) |
|||
) |
|||
}, |
|||
|
|||
toElement: function(){ |
|||
return this.el; |
|||
} |
|||
|
|||
}); |
|||
|
|||
var FileSelect = new Class({ |
|||
|
|||
multiple: function(type, files, single){ |
|||
|
|||
var results = files.filter(function(file){ |
|||
return file.type_id == File.Type.get(type).id; |
|||
}); |
|||
|
|||
if(single){ |
|||
results = new File(results.pop()); |
|||
} |
|||
else { |
|||
|
|||
} |
|||
|
|||
return results; |
|||
|
|||
}, |
|||
|
|||
single: function(type, files){ |
|||
return this.multiple(type, files, true); |
|||
} |
|||
|
|||
}); |
|||
window.File.Select = new FileSelect(); |
|||
|
|||
var FileTypeBase = new Class({ |
|||
|
|||
setup: function(types){ |
|||
var self = this; |
|||
|
|||
self.typesById = {}; |
|||
self.typesByKey = {}; |
|||
Object.each(types, function(type){ |
|||
self.typesByKey[type.identifier] = type; |
|||
self.typesById[type.id] = type; |
|||
}); |
|||
|
|||
}, |
|||
|
|||
get: function(identifier){ |
|||
if(typeOf(identifier) == 'number') |
|||
return this.typesById[identifier] |
|||
else |
|||
return this.typesByKey[identifier] |
|||
} |
|||
|
|||
}); |
|||
window.File.Type = new FileTypeBase(); |
@ -0,0 +1,11 @@ |
|||
var StatusBase = new Class({ |
|||
|
|||
setup: function(statuses){ |
|||
var self = this; |
|||
|
|||
self.statuses = statuses; |
|||
|
|||
} |
|||
|
|||
}); |
|||
window.Status = new StatusBase(); |
@ -0,0 +1,21 @@ |
|||
/* @override http://localhost:5000/static/style/plugin/quality.css */ |
|||
|
|||
|
|||
.profile > .delete { |
|||
background-position: center; |
|||
height: 20px; |
|||
width: 20px; |
|||
} |
|||
|
|||
.profile .types .type .handle { |
|||
background: url('../../images/handle.png') center; |
|||
display: inline-block; |
|||
height: 20px; |
|||
width: 20px; |
|||
} |
|||
|
|||
.profile .types .type .delete { |
|||
background-position: center; |
|||
height: 20px; |
|||
width: 20px; |
|||
} |
@ -1,30 +0,0 @@ |
|||
#!/usr/bin/env python |
|||
"""You need to have setuptools installed. |
|||
|
|||
Usage: |
|||
python setup.py develop |
|||
|
|||
This will register the couchpotato package in your system and thereby make it |
|||
available from anywhere. |
|||
|
|||
Also, a script will be installed to control couchpotato from the shell. |
|||
Try running: |
|||
couchpotato --help |
|||
|
|||
""" |
|||
|
|||
from setuptools import setup |
|||
|
|||
setup(name="couchpotato", |
|||
packages=['couchpotato'], |
|||
install_requires=[ |
|||
'argparse', |
|||
'elixir', |
|||
'flask', |
|||
'nose', |
|||
'sqlalchemy'], |
|||
entry_points=""" |
|||
[console_scripts] |
|||
couchpotato = couchpotato.cli:cmd_couchpotato |
|||
""") |
|||
|
Loading…
Reference in new issue