Merge branch 'refs/heads/develop'
This commit is contained in:
@@ -37,8 +37,11 @@ class Growl(Notification):
|
||||
)
|
||||
self.growl.register()
|
||||
self.registered = True
|
||||
except:
|
||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||
except Exception, e:
|
||||
if 'timed out' in str(e):
|
||||
self.registered = True
|
||||
else:
|
||||
log.error('Failed register of growl: %s', traceback.format_exc())
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from .main import Toasty
|
||||
|
||||
def start():
|
||||
return Toasty()
|
||||
|
||||
config = [{
|
||||
'name': 'toasty',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'notifications',
|
||||
'name': 'toasty',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
'default': 0,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'api_key',
|
||||
'label': 'Device ID',
|
||||
},
|
||||
{
|
||||
'name': 'on_snatch',
|
||||
'default': 0,
|
||||
'type': 'bool',
|
||||
'advanced': True,
|
||||
'description': 'Also send message when movie is snatched.',
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,30 @@
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.notifications.base import Notification
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
class Toasty(Notification):
|
||||
|
||||
urls = {
|
||||
'api': 'http://api.supertoasty.com/notify/%s?%s'
|
||||
}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
data = {
|
||||
'title': self.default_title,
|
||||
'text': toUnicode(message),
|
||||
'sender': toUnicode("CouchPotato"),
|
||||
'image': 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/homescreen.png',
|
||||
}
|
||||
|
||||
try:
|
||||
self.urlopen(self.urls['api'] % (self.conf('api_key'), tryUrlencode(data)), show_error = False)
|
||||
return True
|
||||
except:
|
||||
log.error('Toasty failed: %s', traceback.format_exc())
|
||||
|
||||
return False
|
||||
@@ -10,6 +10,7 @@ config = [{
|
||||
'tab': 'notifications',
|
||||
'name': 'xbmc',
|
||||
'label': 'XBMC',
|
||||
'description': 'v11 (Eden) and v12 (Frodo)',
|
||||
'options': [
|
||||
{
|
||||
'name': 'enabled',
|
||||
|
||||
@@ -4,6 +4,7 @@ from couchpotato.core.notifications.base import Notification
|
||||
from flask.helpers import json
|
||||
import base64
|
||||
import traceback
|
||||
import urllib
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -11,27 +12,147 @@ log = CPLog(__name__)
|
||||
class XBMC(Notification):
|
||||
|
||||
listen_to = ['renamer.after']
|
||||
use_json_notifications = {}
|
||||
|
||||
def notify(self, message = '', data = {}, listener = None):
|
||||
if self.isDisabled(): return
|
||||
|
||||
hosts = splitString(self.conf('host'))
|
||||
|
||||
successful = 0
|
||||
for host in hosts:
|
||||
response = self.request(host, [
|
||||
('GUI.ShowNotification', {"title":"CouchPotato", "message":message}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
|
||||
if self.use_json_notifications.get(host) is None:
|
||||
self.getXBMCJSONversion(host, message = message)
|
||||
|
||||
if self.use_json_notifications.get(host):
|
||||
response = self.request(host, [
|
||||
('GUI.ShowNotification', {'title':self.default_title, 'message':message}),
|
||||
('VideoLibrary.Scan', {}),
|
||||
])
|
||||
else:
|
||||
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||
response += self.request(host, [('VideoLibrary.Scan', {})])
|
||||
|
||||
try:
|
||||
for result in response:
|
||||
if result['result'] == "OK":
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
successful += 1
|
||||
elif (result.get('error')):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
|
||||
except:
|
||||
log.error('Failed parsing results: %s', traceback.format_exc())
|
||||
|
||||
return successful == len(hosts) * 2
|
||||
|
||||
def getXBMCJSONversion(self, host, message = ''):
|
||||
|
||||
success = False
|
||||
|
||||
# XBMC JSON-RPC version request
|
||||
response = self.request(host, [
|
||||
('JSONRPC.Version', {})
|
||||
])
|
||||
for result in response:
|
||||
if (result.get('result') and type(result['result']['version']).__name__ == 'int'):
|
||||
# only v2 and v4 return an int object
|
||||
# v6 (as of XBMC v12(Frodo)) is required to send notifications
|
||||
xbmc_rpc_version = str(result['result']['version'])
|
||||
|
||||
log.debug('XBMC JSON-RPC Version: %s ; Notifications by JSON-RPC only supported for v6 [as of XBMC v12(Frodo)]', xbmc_rpc_version)
|
||||
|
||||
# disable JSON use
|
||||
self.use_json_notifications[host] = False
|
||||
|
||||
# send the text message
|
||||
resp = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message})
|
||||
for result in resp:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
log.debug('Message delivered successfully!')
|
||||
success = True
|
||||
break
|
||||
elif (result.get('error')):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
break
|
||||
|
||||
elif (result.get('result') and type(result['result']['version']).__name__ == 'dict'):
|
||||
# XBMC JSON-RPC v6 returns an array object containing
|
||||
# major, minor and patch number
|
||||
xbmc_rpc_version = str(result['result']['version']['major'])
|
||||
xbmc_rpc_version += '.' + str(result['result']['version']['minor'])
|
||||
xbmc_rpc_version += '.' + str(result['result']['version']['patch'])
|
||||
|
||||
log.debug('XBMC JSON-RPC Version: %s', xbmc_rpc_version)
|
||||
|
||||
# ok, XBMC version is supported
|
||||
self.use_json_notifications[host] = True
|
||||
|
||||
# send the text message
|
||||
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message})])
|
||||
for result in resp:
|
||||
if (result.get('result') and result['result'] == 'OK'):
|
||||
log.debug('Message delivered successfully!')
|
||||
success = True
|
||||
break
|
||||
elif (result.get('error')):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
break
|
||||
|
||||
# error getting version info (we do have contact with XBMC though)
|
||||
elif (result.get('error')):
|
||||
log.error('XBMC error; %s: %s (%s)', (result['id'], result['error']['message'], result['error']['code']))
|
||||
|
||||
log.debug('Use JSON notifications: %s ', self.use_json_notifications)
|
||||
|
||||
return success
|
||||
|
||||
def notifyXBMCnoJSON(self, host, data):
|
||||
|
||||
server = 'http://%s/xbmcCmds/' % host
|
||||
|
||||
# title, message [, timeout , image #can be added!]
|
||||
cmd = "xbmcHttp?command=ExecBuiltIn(Notification('%s','%s'))" % (urllib.quote(data['title']), urllib.quote(data['message']))
|
||||
server += cmd
|
||||
|
||||
# I have no idea what to set to, just tried text/plain and seems to be working :)
|
||||
headers = {
|
||||
'Content-Type': 'text/plain',
|
||||
}
|
||||
|
||||
# authentication support
|
||||
if self.conf('password'):
|
||||
base64string = base64.encodestring('%s:%s' % (self.conf('username'), self.conf('password'))).replace('\n', '')
|
||||
headers['Authorization'] = 'Basic %s' % base64string
|
||||
|
||||
try:
|
||||
log.debug('Sending non-JSON-type request to %s: %s', (host, data))
|
||||
|
||||
# response wil either be 'OK':
|
||||
# <html>
|
||||
# <li>OK
|
||||
# </html>
|
||||
#
|
||||
# or 'Error':
|
||||
# <html>
|
||||
# <li>Error:<message>
|
||||
# </html>
|
||||
#
|
||||
response = self.urlopen(server, headers = headers)
|
||||
|
||||
if 'OK' in response:
|
||||
log.debug('Returned from non-JSON-type request %s: %s', (host, response))
|
||||
# manually fake expected response array
|
||||
return [{'result': 'OK'}]
|
||||
else:
|
||||
log.error('Returned from non-JSON-type request %s: %s', (host, response))
|
||||
# manually fake expected response array
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
except:
|
||||
log.error('Failed sending non-JSON-type request to XBMC: %s', traceback.format_exc())
|
||||
return [{'result': 'Error'}]
|
||||
|
||||
def request(self, host, requests):
|
||||
server = 'http://%s/jsonrpc' % host
|
||||
|
||||
|
||||
@@ -341,7 +341,7 @@ class Scanner(Plugin):
|
||||
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
|
||||
|
||||
if len(group['files']['movie']) == 0:
|
||||
log.error('Couldn\t find any movie files for %s', identifier)
|
||||
log.error('Couldn\'t find any movie files for %s', identifier)
|
||||
continue
|
||||
|
||||
log.debug('Getting metadata for %s', identifier)
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from .main import ITunes
|
||||
|
||||
def start():
|
||||
return ITunes()
|
||||
|
||||
config = [{
|
||||
'name': 'itunes',
|
||||
'groups': [
|
||||
{
|
||||
'tab': 'automation',
|
||||
'name': 'itunes_automation',
|
||||
'label': 'iTunes',
|
||||
'description': 'From any <a href="http://itunes.apple.com/rss">iTunes</a> Store feed. Url should be the RSS link. (uses minimal requirements)',
|
||||
'options': [
|
||||
{
|
||||
'name': 'automation_enabled',
|
||||
'default': False,
|
||||
'type': 'enabler',
|
||||
},
|
||||
{
|
||||
'name': 'automation_urls_use',
|
||||
'label': 'Use',
|
||||
'default': ',',
|
||||
},
|
||||
{
|
||||
'name': 'automation_urls',
|
||||
'label': 'url',
|
||||
'type': 'combined',
|
||||
'combine': ['automation_urls_use', 'automation_urls'],
|
||||
'default': 'https://itunes.apple.com/rss/topmovies/limit=25/xml,',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}]
|
||||
@@ -0,0 +1,63 @@
|
||||
from couchpotato.core.helpers.rss import RSS
|
||||
from couchpotato.core.helpers.variable import md5, splitString, tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.automation.base import Automation
|
||||
from xml.etree.ElementTree import QName
|
||||
import datetime
|
||||
import traceback
|
||||
import xml.etree.ElementTree as XMLTree
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
|
||||
class ITunes(Automation, RSS):
|
||||
|
||||
interval = 1800
|
||||
|
||||
def getIMDBids(self):
|
||||
|
||||
if self.isDisabled():
|
||||
return
|
||||
|
||||
movies = []
|
||||
|
||||
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
|
||||
urls = splitString(self.conf('automation_urls'))
|
||||
|
||||
namespace = 'http://www.w3.org/2005/Atom'
|
||||
namespaceIM = 'http://itunes.apple.com/rss'
|
||||
|
||||
index = -1
|
||||
for url in urls:
|
||||
|
||||
index += 1
|
||||
if not enablers[index]:
|
||||
continue
|
||||
|
||||
try:
|
||||
cache_key = 'itunes.rss.%s' % md5(url)
|
||||
rss_data = self.getCache(cache_key, url)
|
||||
|
||||
data = XMLTree.fromstring(rss_data)
|
||||
|
||||
if data is not None:
|
||||
entry_tag = str(QName(namespace, 'entry'))
|
||||
rss_movies = self.getElements(data, entry_tag)
|
||||
|
||||
for movie in rss_movies:
|
||||
name_tag = str(QName(namespaceIM, 'name'))
|
||||
name = self.getTextElement(movie, name_tag)
|
||||
|
||||
releaseDate_tag = str(QName(namespaceIM, 'releaseDate'))
|
||||
releaseDateText = self.getTextElement(movie, releaseDate_tag)
|
||||
year = datetime.datetime.strptime(releaseDateText, '%Y-%m-%dT00:00:00-07:00').strftime("%Y")
|
||||
|
||||
imdb = self.search(name, year)
|
||||
|
||||
if imdb and self.isMinimalMovie(imdb):
|
||||
movies.append(imdb['imdb'])
|
||||
|
||||
except:
|
||||
log.error('Failed loading iTunes rss feed: %s %s', (url, traceback.format_exc()))
|
||||
|
||||
return movies
|
||||
@@ -95,15 +95,20 @@ class YarrProvider(Provider):
|
||||
urllib2.install_opener(opener)
|
||||
log.info2('Logging into %s', self.urls['login'])
|
||||
f = opener.open(self.urls['login'], self.getLoginParams())
|
||||
f.read()
|
||||
output = f.read()
|
||||
f.close()
|
||||
self.login_opener = opener
|
||||
return True
|
||||
|
||||
if self.loginSuccess(output):
|
||||
self.login_opener = opener
|
||||
return True
|
||||
except:
|
||||
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
|
||||
|
||||
return False
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return True
|
||||
|
||||
def loginDownload(self, url = '', nzb_id = ''):
|
||||
try:
|
||||
if not self.login_opener and not self.login():
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
from bs4 import BeautifulSoup
|
||||
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
|
||||
from couchpotato.core.helpers.variable import tryInt
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.core.providers.nzb.base import NZBProvider
|
||||
from couchpotato.environment import Env
|
||||
from dateutil.parser import parse
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
log = CPLog(__name__)
|
||||
|
||||
@@ -14,7 +12,7 @@ log = CPLog(__name__)
|
||||
class FTDWorld(NZBProvider):
|
||||
|
||||
urls = {
|
||||
'search': 'http://ftdworld.net/category.php?%s',
|
||||
'search': 'http://ftdworld.net/api/index.php?%s',
|
||||
'detail': 'http://ftdworld.net/spotinfo.php?id=%s',
|
||||
'download': 'http://ftdworld.net/cgi-bin/nzbdown.pl?fileID=%s',
|
||||
'login': 'http://ftdworld.net/index.php',
|
||||
@@ -25,7 +23,7 @@ class FTDWorld(NZBProvider):
|
||||
cat_ids = [
|
||||
([4, 11], ['dvdr']),
|
||||
([1], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr', 'brrip']),
|
||||
([10, 13, 14], ['bd50', '720p', '1080p']),
|
||||
([7, 10, 13, 14], ['bd50', '720p', '1080p']),
|
||||
]
|
||||
cat_backup_id = 1
|
||||
|
||||
@@ -43,38 +41,29 @@ class FTDWorld(NZBProvider):
|
||||
'ctype': ','.join([str(x) for x in self.getCatId(quality['identifier'])]),
|
||||
})
|
||||
|
||||
data = self.getHTMLData(self.urls['search'] % params, opener = self.login_opener)
|
||||
data = self.getJsonData(self.urls['search'] % params, opener = self.login_opener)
|
||||
|
||||
if data:
|
||||
try:
|
||||
|
||||
html = BeautifulSoup(data)
|
||||
main_table = html.find('table', attrs = {'id':'ftdresult'})
|
||||
|
||||
if not main_table:
|
||||
if data.get('numRes') == 0:
|
||||
return
|
||||
|
||||
items = main_table.find_all('tr', attrs = {'class': re.compile('tcontent')})
|
||||
|
||||
for item in items:
|
||||
tds = item.find_all('td')
|
||||
nzb_id = tryInt(item.attrs['data-spot'])
|
||||
|
||||
up = item.find('img', attrs = {'src': re.compile('up.png')})
|
||||
down = item.find('img', attrs = {'src': re.compile('down.png')})
|
||||
for item in data.get('data'):
|
||||
|
||||
nzb_id = tryInt(item.get('id'))
|
||||
results.append({
|
||||
'id': nzb_id,
|
||||
'name': toUnicode(item.find('a', attrs = {'href': re.compile('./spotinfo')}).text.strip()),
|
||||
'age': self.calculateAge(int(time.mktime(parse(tds[2].text).timetuple()))),
|
||||
'name': toUnicode(item.get('Title')),
|
||||
'age': self.calculateAge(tryInt(item.get('Created'))),
|
||||
'url': self.urls['download'] % nzb_id,
|
||||
'download': self.loginDownload,
|
||||
'detail_url': self.urls['detail'] % nzb_id,
|
||||
'score': (tryInt(up.attrs['title'].split(' ')[0]) * 3) - (tryInt(down.attrs['title'].split(' ')[0]) * 3) if up else 0,
|
||||
'score': (tryInt(item.get('webPlus', 0)) - tryInt(item.get('webMin', 0))) * 3,
|
||||
})
|
||||
|
||||
except:
|
||||
log.error('Failed to parse HTML response from FTDWorld')
|
||||
log.error('Failed to parse HTML response from FTDWorld: %s', traceback.format_exc())
|
||||
|
||||
def getLoginParams(self):
|
||||
return tryUrlencode({
|
||||
@@ -82,3 +71,6 @@ class FTDWorld(NZBProvider):
|
||||
'passlogin': self.conf('password'),
|
||||
'submit': 'Log In',
|
||||
})
|
||||
|
||||
def loginSuccess(self, output):
|
||||
return 'password is incorrect' not in output
|
||||
|
||||
@@ -136,6 +136,6 @@ class Newznab(NZBProvider, RSS):
|
||||
self.limits_reached[host] = time.time()
|
||||
return 'try_next'
|
||||
|
||||
log.error('Failed download from %s', (host, traceback.format_exc()))
|
||||
log.error('Failed download from %s: %s', (host, traceback.format_exc()))
|
||||
|
||||
return 'try_next'
|
||||
|
||||
Executable → Regular
+33
-76
@@ -1,92 +1,49 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# PROVIDE: couchpotato
|
||||
# REQUIRE: sabnzbd
|
||||
# REQUIRE: DAEMON
|
||||
# KEYWORD: shutdown
|
||||
#
|
||||
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
|
||||
# to enable this service:
|
||||
#
|
||||
# couchpotato_enable (bool): Set to NO by default.
|
||||
# Set it to YES to enable it.
|
||||
# couchpotato_user: The user account CouchPotato daemon runs as what
|
||||
# you want it to be. It uses '_sabnzbd' user by
|
||||
# default. Do not sets it as empty or it will run
|
||||
# as root.
|
||||
# couchpotato_dir: Directory where CouchPotato lives.
|
||||
# Default: /usr/local/couchpotato
|
||||
# couchpotato_chdir: Change to this directory before running CouchPotato.
|
||||
# Default is same as couchpotato_dir.
|
||||
# couchpotato_pid: The name of the pidfile to create.
|
||||
# Default is couchpotato.pid in couchpotato_dir.
|
||||
|
||||
# Add the following lines to /etc/rc.conf to enable couchpotato:
|
||||
# couchpotato_enable: Set to NO by default. Set it to YES to enable it.
|
||||
# couchpotato_user: The user account CouchPotato daemon runs as what
|
||||
# you want it to be.
|
||||
# couchpotato_dir: Directory where CouchPotato lives.
|
||||
# Default: /usr/local/CouchPotatoServer
|
||||
# couchpotato_datadir: Directory where CouchPotato user data lives.
|
||||
# Default: $couchpotato_dir/data
|
||||
# couchpotato_conf: Directory where CouchPotato user data lives.
|
||||
# Default: $couchpotato_datadir/settings.conf
|
||||
# couchpotato_pid: Full path to PID file.
|
||||
# Default: $couchpotato_datadir/couchpotato.pid
|
||||
# couchpotato_flags: Set additonal flags as needed.
|
||||
|
||||
. /etc/rc.subr
|
||||
|
||||
name="couchpotato"
|
||||
rcvar=${name}_enable
|
||||
|
||||
# Required, for some reason, to find all our binaries when starting via service.
|
||||
PATH="/usr/bin:/usr/local/bin:$PATH"
|
||||
rcvar=couchpotato_enable
|
||||
|
||||
load_rc_config ${name}
|
||||
|
||||
: ${couchpotato_enable:="NO"}
|
||||
: ${couchpotato_user:="_sabnzbd"}
|
||||
: ${couchpotato_dir:="/usr/local/couchpotato"}
|
||||
: ${couchpotato_chdir:="${couchpotato_dir}"}
|
||||
: ${couchpotato_pid:="${couchpotato_dir}/couchpotato.pid"}
|
||||
: ${couchpotato_conf:="${couchpotato_dir}/data/settings.conf"}
|
||||
: ${couchpotato_enable:=NO}
|
||||
: ${couchpotato_user:=} #default is root
|
||||
: ${couchpotato_dir:=/usr/local/CouchPotatoServer}
|
||||
: ${couchpotato_datadir:=${couchpotato_dir}/data}
|
||||
: ${couchpotato_conf:=} #default is datadir/settings.conf
|
||||
: ${couchpotato_pid:=} #default is datadir/couchpotato.pid
|
||||
: ${couchpotato_flags:=}
|
||||
|
||||
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown CouchPotato.
|
||||
if [ -e "${couchpotato_conf}" ]; then
|
||||
HOST=`grep -A14 "\[core\]" "${couchpotato_conf}"|awk -F" = " '/^host/ {print $2}'`
|
||||
PORT=`grep -A14 "\[core\]" "${couchpotato_conf}"|awk -F" = " '/^port/ {print $2}'`
|
||||
CPAPI=`grep -A14 "\[core\]" "${couchpotato_conf}"|awk -F" = " '/^api_key/ {print $2}'`
|
||||
command="${couchpotato_dir}/CouchPotato.py"
|
||||
command_interpreter="/usr/local/bin/python"
|
||||
command_args="--daemon --data_dir ${couchpotato_datadir}"
|
||||
|
||||
# append optional flags
|
||||
if [ -n "${couchpotato_pid}" ]; then
|
||||
pidfile=${couchpotato_pid}
|
||||
couchpotato_flags="${couchpotato_flags} --pid_file ${couchpotato_pid}"
|
||||
fi
|
||||
|
||||
status_cmd="${name}_status"
|
||||
stop_cmd="${name}_stop"
|
||||
|
||||
command="/usr/sbin/daemon"
|
||||
command_args="-f -p ${couchpotato_pid} python ${couchpotato_dir}/CouchPotato.py ${couchpotato_flags}"
|
||||
|
||||
# Check for wget and refuse to start without it.
|
||||
if [ ! -x "${WGET}" ]; then
|
||||
warn "couchpotato not started: You need wget to safely shut down CouchPotato."
|
||||
exit 1
|
||||
if [ -n "${couchpotato_conf}" ]; then
|
||||
couchpotato_flags="${couchpotato_flags} --config_file ${couchpotato_conf}"
|
||||
fi
|
||||
|
||||
# Ensure user is root when running this script.
|
||||
if [ `id -u` != "0" ]; then
|
||||
echo "Oops, you should be root before running this!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
verify_couchpotato_pid() {
|
||||
# Make sure the pid corresponds to the CouchPotato process.
|
||||
pid=`cat ${couchpotato_pid} 2>/dev/null`
|
||||
ps -p ${pid} | grep -q "python ${couchpotato_dir}/CouchPotato.py"
|
||||
return $?
|
||||
}
|
||||
|
||||
# Try to stop CouchPotato cleanly by calling shutdown over http.
|
||||
couchpotato_stop() {
|
||||
if [ ! -e "${couchpotato_conf}" ]; then
|
||||
echo "CouchPotato's settings file does not exist. Try starting CouchPotato, as this should create the file."
|
||||
exit 1
|
||||
fi
|
||||
echo "Stopping $name"
|
||||
verify_couchpotato_pid
|
||||
${WGET} -O - -q "http://${HOST}:${PORT}/api/${CPAPI}/app.shutdown/" >/dev/null
|
||||
if [ -n "${pid}" ]; then
|
||||
wait_for_pids ${pid}
|
||||
echo "Stopped"
|
||||
fi
|
||||
}
|
||||
|
||||
couchpotato_status() {
|
||||
verify_couchpotato_pid && echo "$name is running as ${pid}" || echo "$name is not running"
|
||||
}
|
||||
|
||||
run_rc_command "$1"
|
||||
|
||||
|
||||
+63
-37
@@ -1,8 +1,9 @@
|
||||
import hashlib
|
||||
import re
|
||||
import hashlib
|
||||
import time
|
||||
import StringIO
|
||||
|
||||
__version__ = '0.6'
|
||||
__version__ = '0.8'
|
||||
|
||||
#GNTP/<version> <messagetype> <encryptionAlgorithmID>[:<ivValue>][ <keyHashAlgorithmID>:<keyHash>.<salt>]
|
||||
GNTP_INFO_LINE = re.compile(
|
||||
@@ -19,7 +20,7 @@ GNTP_INFO_LINE_SHORT = re.compile(
|
||||
|
||||
GNTP_HEADER = re.compile('([\w-]+):(.+)')
|
||||
|
||||
GNTP_EOL = u'\r\n'
|
||||
GNTP_EOL = '\r\n'
|
||||
|
||||
|
||||
class BaseError(Exception):
|
||||
@@ -43,6 +44,14 @@ class UnsupportedError(BaseError):
|
||||
errordesc = 'Currently unsupported by gntp.py'
|
||||
|
||||
|
||||
class _GNTPBuffer(StringIO.StringIO):
|
||||
"""GNTP Buffer class"""
|
||||
def writefmt(self, message = "", *args):
|
||||
"""Shortcut function for writing GNTP Headers"""
|
||||
self.write((message % args).encode('utf8', 'replace'))
|
||||
self.write(GNTP_EOL)
|
||||
|
||||
|
||||
class _GNTPBase(object):
|
||||
"""Base initilization
|
||||
|
||||
@@ -206,8 +215,8 @@ class _GNTPBase(object):
|
||||
if not match:
|
||||
continue
|
||||
|
||||
key = match.group(1).strip()
|
||||
val = match.group(2).strip()
|
||||
key = unicode(match.group(1).strip(), 'utf8', 'replace')
|
||||
val = unicode(match.group(2).strip(), 'utf8', 'replace')
|
||||
dict[key] = val
|
||||
return dict
|
||||
|
||||
@@ -217,6 +226,15 @@ class _GNTPBase(object):
|
||||
else:
|
||||
self.headers[key] = unicode('%s' % value, 'utf8', 'replace')
|
||||
|
||||
def add_resource(self, data):
|
||||
"""Add binary resource
|
||||
|
||||
:param string data: Binary Data
|
||||
"""
|
||||
identifier = hashlib.md5(data).hexdigest()
|
||||
self.resources[identifier] = data
|
||||
return 'x-growl-resource://%s' % identifier
|
||||
|
||||
def decode(self, data, password = None):
|
||||
"""Decode GNTP Message
|
||||
|
||||
@@ -229,19 +247,30 @@ class _GNTPBase(object):
|
||||
self.headers = self._parse_dict(parts[0])
|
||||
|
||||
def encode(self):
|
||||
"""Encode a GNTP Message
|
||||
"""Encode a generic GNTP Message
|
||||
|
||||
:return string: Encoded GNTP Message ready to be sent
|
||||
:return string: GNTP Message ready to be sent
|
||||
"""
|
||||
self.validate()
|
||||
|
||||
message = self._format_info() + GNTP_EOL
|
||||
buffer = _GNTPBuffer()
|
||||
|
||||
buffer.writefmt(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.iteritems():
|
||||
message += u'%s: %s%s' % (k, v, GNTP_EOL)
|
||||
buffer.writefmt('%s: %s', k, v)
|
||||
buffer.writefmt()
|
||||
|
||||
message += GNTP_EOL
|
||||
return message
|
||||
#Resources
|
||||
for resource, data in self.resources.iteritems():
|
||||
buffer.writefmt('Identifier: %s', resource)
|
||||
buffer.writefmt('Length: %d', len(data))
|
||||
buffer.writefmt()
|
||||
buffer.write(data)
|
||||
buffer.writefmt()
|
||||
buffer.writefmt()
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class GNTPRegister(_GNTPBase):
|
||||
@@ -290,7 +319,7 @@ class GNTPRegister(_GNTPBase):
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
@@ -319,22 +348,33 @@ class GNTPRegister(_GNTPBase):
|
||||
|
||||
:return string: Encoded GNTP Registration message
|
||||
"""
|
||||
self.validate()
|
||||
|
||||
message = self._format_info() + GNTP_EOL
|
||||
buffer = _GNTPBuffer()
|
||||
|
||||
buffer.writefmt(self._format_info())
|
||||
|
||||
#Headers
|
||||
for k, v in self.headers.iteritems():
|
||||
message += u'%s: %s%s' % (k, v, GNTP_EOL)
|
||||
buffer.writefmt('%s: %s', k, v)
|
||||
buffer.writefmt()
|
||||
|
||||
#Notifications
|
||||
if len(self.notifications) > 0:
|
||||
for notice in self.notifications:
|
||||
message += GNTP_EOL
|
||||
for k, v in notice.iteritems():
|
||||
message += u'%s: %s%s' % (k, v, GNTP_EOL)
|
||||
buffer.writefmt('%s: %s', k, v)
|
||||
buffer.writefmt()
|
||||
|
||||
message += GNTP_EOL
|
||||
return message
|
||||
#Resources
|
||||
for resource, data in self.resources.iteritems():
|
||||
buffer.writefmt('Identifier: %s', resource)
|
||||
buffer.writefmt('Length: %d', len(data))
|
||||
buffer.writefmt()
|
||||
buffer.write(data)
|
||||
buffer.writefmt()
|
||||
buffer.writefmt()
|
||||
|
||||
return buffer.getvalue()
|
||||
|
||||
|
||||
class GNTPNotice(_GNTPBase):
|
||||
@@ -379,7 +419,7 @@ class GNTPNotice(_GNTPBase):
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
if i == 0:
|
||||
continue # Skip Header
|
||||
continue # Skip Header
|
||||
if part.strip() == '':
|
||||
continue
|
||||
notice = self._parse_dict(part)
|
||||
@@ -388,21 +428,6 @@ class GNTPNotice(_GNTPBase):
|
||||
#open('notice.png','wblol').write(notice['Data'])
|
||||
self.resources[notice.get('Identifier')] = notice
|
||||
|
||||
def encode(self):
|
||||
"""Encode a GNTP Notification Message
|
||||
|
||||
:return string: GNTP Notification Message ready to be sent
|
||||
"""
|
||||
self.validate()
|
||||
|
||||
message = self._format_info() + GNTP_EOL
|
||||
#Headers
|
||||
for k, v in self.headers.iteritems():
|
||||
message += u'%s: %s%s' % (k, v, GNTP_EOL)
|
||||
|
||||
message += GNTP_EOL
|
||||
return message
|
||||
|
||||
|
||||
class GNTPSubscribe(_GNTPBase):
|
||||
"""Represents a GNTP Subscribe Command
|
||||
@@ -457,7 +482,8 @@ class GNTPError(_GNTPBase):
|
||||
self.add_header('Error-Description', errordesc)
|
||||
|
||||
def error(self):
|
||||
return self.headers['Error-Code'], self.headers['Error-Description']
|
||||
return (self.headers.get('Error-Code', None),
|
||||
self.headers.get('Error-Description', None))
|
||||
|
||||
|
||||
def parse_gntp(data, password = None):
|
||||
|
||||
+63
-46
@@ -22,43 +22,6 @@ __all__ = [
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def mini(description, applicationName = 'PythonMini', noteType = "Message",
|
||||
title = "Mini Message", applicationIcon = None, hostname = 'localhost',
|
||||
password = None, port = 23053, sticky = False, priority = None,
|
||||
callback = None):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
growl = GrowlNotifier(
|
||||
applicationName = applicationName,
|
||||
notifications = [noteType],
|
||||
defaultNotifications = [noteType],
|
||||
hostname = hostname,
|
||||
password = password,
|
||||
port = port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
return result
|
||||
|
||||
return growl.notify(
|
||||
noteType = noteType,
|
||||
title = title,
|
||||
description = description,
|
||||
icon = applicationIcon,
|
||||
sticky = sticky,
|
||||
priority = priority,
|
||||
callback = callback,
|
||||
)
|
||||
|
||||
|
||||
class GrowlNotifier(object):
|
||||
"""Helper class to simplfy sending Growl messages
|
||||
|
||||
@@ -93,10 +56,12 @@ class GrowlNotifier(object):
|
||||
def _checkIcon(self, data):
|
||||
'''
|
||||
Check the icon to see if it's valid
|
||||
@param data:
|
||||
@todo Consider checking for a valid URL
|
||||
|
||||
If it's a simple URL icon, then we return True. If it's a data icon
|
||||
then we return False
|
||||
'''
|
||||
return data
|
||||
logger.info('Checking icon')
|
||||
return data.startswith('http')
|
||||
|
||||
def register(self):
|
||||
"""Send GNTP Registration
|
||||
@@ -112,7 +77,11 @@ class GrowlNotifier(object):
|
||||
enabled = notification in self.defaultNotifications
|
||||
register.add_notification(notification, enabled)
|
||||
if self.applicationIcon:
|
||||
register.add_header('Application-Icon', self.applicationIcon)
|
||||
if self._checkIcon(self.applicationIcon):
|
||||
register.add_header('Application-Icon', self.applicationIcon)
|
||||
else:
|
||||
id = register.add_resource(self.applicationIcon)
|
||||
register.add_header('Application-Icon', id)
|
||||
if self.password:
|
||||
register.set_password(self.password, self.passwordHash)
|
||||
self.add_origin_info(register)
|
||||
@@ -120,7 +89,7 @@ class GrowlNotifier(object):
|
||||
return self._send('register', register)
|
||||
|
||||
def notify(self, noteType, title, description, icon = None, sticky = False,
|
||||
priority = None, callback = None):
|
||||
priority = None, callback = None, identifier = None):
|
||||
"""Send a GNTP notifications
|
||||
|
||||
.. warning::
|
||||
@@ -151,11 +120,18 @@ class GrowlNotifier(object):
|
||||
if priority:
|
||||
notice.add_header('Notification-Priority', priority)
|
||||
if icon:
|
||||
notice.add_header('Notification-Icon', self._checkIcon(icon))
|
||||
if self._checkIcon(icon):
|
||||
notice.add_header('Notification-Icon', icon)
|
||||
else:
|
||||
id = notice.add_resource(icon)
|
||||
notice.add_header('Notification-Icon', id)
|
||||
|
||||
if description:
|
||||
notice.add_header('Notification-Text', description)
|
||||
if callback:
|
||||
notice.add_header('Notification-Callback-Target', callback)
|
||||
if identifier:
|
||||
notice.add_header('Notification-Coalescing-ID', identifier)
|
||||
|
||||
self.add_origin_info(notice)
|
||||
self.notify_hook(notice)
|
||||
@@ -193,9 +169,10 @@ class GrowlNotifier(object):
|
||||
def subscribe_hook(self, packet):
|
||||
pass
|
||||
|
||||
def _send(self, type, packet):
|
||||
def _send(self, messagetype, packet):
|
||||
"""Send the GNTP Packet"""
|
||||
|
||||
packet.validate()
|
||||
data = packet.encode()
|
||||
|
||||
logger.debug('To : %s:%s <%s>\n%s', self.hostname, self.port, packet.__class__, data)
|
||||
@@ -203,7 +180,7 @@ class GrowlNotifier(object):
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
s.settimeout(self.socketTimeout)
|
||||
s.connect((self.hostname, self.port))
|
||||
s.send(data.encode('utf8', 'replace'))
|
||||
s.send(data)
|
||||
recv_data = s.recv(1024)
|
||||
while not recv_data.endswith("\r\n\r\n"):
|
||||
recv_data += s.recv(1024)
|
||||
@@ -212,11 +189,51 @@ class GrowlNotifier(object):
|
||||
|
||||
logger.debug('From : %s:%s <%s>\n%s', self.hostname, self.port, response.__class__, response)
|
||||
|
||||
if response.info['messagetype'] == '-OK':
|
||||
if type(response) == gntp.GNTPOK:
|
||||
return True
|
||||
logger.error('Invalid response: %s', response.error())
|
||||
return response.error()
|
||||
|
||||
|
||||
def mini(description, applicationName = 'PythonMini', noteType = "Message",
|
||||
title = "Mini Message", applicationIcon = None, hostname = 'localhost',
|
||||
password = None, port = 23053, sticky = False, priority = None,
|
||||
callback = None, notificationIcon = None, identifier = None,
|
||||
notifierFactory = GrowlNotifier):
|
||||
"""Single notification function
|
||||
|
||||
Simple notification function in one line. Has only one required parameter
|
||||
and attempts to use reasonable defaults for everything else
|
||||
:param string description: Notification message
|
||||
|
||||
.. warning::
|
||||
For now, only URL callbacks are supported. In the future, the
|
||||
callback argument will also support a function
|
||||
"""
|
||||
growl = notifierFactory(
|
||||
applicationName = applicationName,
|
||||
notifications = [noteType],
|
||||
defaultNotifications = [noteType],
|
||||
applicationIcon = applicationIcon,
|
||||
hostname = hostname,
|
||||
password = password,
|
||||
port = port,
|
||||
)
|
||||
result = growl.register()
|
||||
if result is not True:
|
||||
return result
|
||||
|
||||
return growl.notify(
|
||||
noteType = noteType,
|
||||
title = title,
|
||||
description = description,
|
||||
icon = notificationIcon,
|
||||
sticky = sticky,
|
||||
priority = priority,
|
||||
callback = callback,
|
||||
identifier = identifier,
|
||||
)
|
||||
|
||||
if __name__ == '__main__':
|
||||
# If we're running this module directly we're likely running it as a test
|
||||
# so extra debugging is useful
|
||||
|
||||
Reference in New Issue
Block a user