Merge branch 'refs/heads/master' into desktop

Conflicts:
	version.py
This commit is contained in:
Ruud committed 2014-03-16 09:43:32 +01:00
commit 66b849cb29
470 files changed
+33120 -15754

No files matched your search

+6 -4
View File
@@ -1,4 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
from __future__ import print_function
from logging import handlers from logging import handlers
from os.path import dirname from os.path import dirname
import logging import logging
@@ -132,14 +133,15 @@ if __name__ == '__main__':
pass pass
except SystemExit: except SystemExit:
raise raise
except socket.error as (nr, msg): except socket.error as e:
# log when socket receives SIGINT, but continue. # log when socket receives SIGINT, but continue.
# previous code would have skipped over other types of IO errors too. # previous code would have skipped over other types of IO errors too.
nr, msg = e
if nr != 4: if nr != 4:
try: try:
l.log.critical(traceback.format_exc()) l.log.critical(traceback.format_exc())
except: except:
print traceback.format_exc() print(traceback.format_exc())
raise raise
except: except:
try: try:
@@ -148,7 +150,7 @@ if __name__ == '__main__':
if l: if l:
l.log.critical(traceback.format_exc()) l.log.critical(traceback.format_exc())
else: else:
print traceback.format_exc() print(traceback.format_exc())
except: except:
print traceback.format_exc() print(traceback.format_exc())
raise raise
+19 -9
View File
@@ -1,15 +1,25 @@
#So you feel like posting a bug, sending me a pull request or just telling me how awesome I am. No problem! ## Got a issue/feature request or submitting a pull request?
##Just make sure you think of the following things: Make sure you think of the following things:
* Search through the existing (and closed) issues first. See if you can get your answer there. ## Issue
* Search through the existing (and closed) issues first, see if you can get your answer there.
* Double check the result manually, because it could be an external issue. * Double check the result manually, because it could be an external issue.
* Post logs! Without seeing what is going on, I can't reproduce the error. * Post logs! Without seeing what is going on, I can't reproduce the error.
* What is the movie + quality you are searching for. * Also check the logs before submitting, obvious errors like permission or http errors are often not related to CP.
* What are you settings for the specific problem. * What is the movie + quality you are searching for?
* What providers are you using. (While your logs include these, scanning through hundred of lines of log isn't my hobby). * What are you're settings for the specific problem?
* Give me a short step by step of how to reproduce. * What providers are you using? (While you're logs include these, scanning through hundred of lines of log isn't our hobby)
* Post the logs from config directory, please do not copy paste the UI. Use pastebin to store these logs!
* Give a short step by step of how to reproduce the error.
* What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example. * What hardware / OS are you using and what are the limits? NAS can be slow and maybe have a different python installed then when you use CP on OSX or Windows for example.
* I will mark issues with the "can't reproduce" tag. Don't go asking me "why closed" if it clearly says the issue in the tag ;) * I will mark issues with the "can't reproduce" tag. Don't go asking "why closed" if it clearly says the issue in the tag ;)
* If you're running on a NAS (QNAP, Austor etc..) with pre-made packages, make sure these are setup to use our source repo (RuudBurger/CouchPotatoServer) and nothing else!!
**If I don't get enough info, the chance of the issue getting closed is a lot bigger ;)** ## Pull Request
* Make sure you're pull request is made for develop branch (or relevant feature branch)
* Have you tested your PR? If not, why?
* Are there any limitations of your PR we should know of?
* Make sure to keep you're PR up-to-date with the branch you're trying to push into.
**If we don't get enough info, the chance of the issue getting closed is a lot bigger ;)**
+19 -18
View File
@@ -9,13 +9,12 @@ import os
import time import time
import traceback import traceback
log = CPLog(__name__)
log = CPLog(__name__)
views = {} views = {}
template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates')) template_loader = template.Loader(os.path.join(os.path.dirname(__file__), 'templates'))
class BaseHandler(RequestHandler): class BaseHandler(RequestHandler):
def get_current_user(self): def get_current_user(self):
@@ -24,9 +23,10 @@ class BaseHandler(RequestHandler):
if username and password: if username and password:
return self.get_secure_cookie('user') return self.get_secure_cookie('user')
else: # Login when no username or password are set else: # Login when no username or password are set
return True return True
# Main web handler # Main web handler
class WebHandler(BaseHandler): class WebHandler(BaseHandler):
@@ -43,11 +43,13 @@ class WebHandler(BaseHandler):
log.error("Failed doing web request '%s': %s", (route, traceback.format_exc())) log.error("Failed doing web request '%s': %s", (route, traceback.format_exc()))
self.write({'success': False, 'error': 'Failed returning results'}) self.write({'success': False, 'error': 'Failed returning results'})
def addView(route, func, static = False): def addView(route, func, static = False):
views[route] = func views[route] = func
def get_session(engine = None):
return Env.getSession(engine) def get_session():
return Env.getSession()
# Web view # Web view
@@ -55,12 +57,10 @@ def index():
return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env) return template_loader.load('index.html').generate(sep = os.sep, fireEvent = fireEvent, Env = Env)
addView('', index) addView('', index)
# API docs # API docs
def apiDocs(): def apiDocs():
routes = [] routes = list(api.keys())
for route in api.iterkeys():
routes.append(route)
if api_docs.get(''): if api_docs.get(''):
del api_docs[''] del api_docs['']
@@ -70,21 +70,22 @@ def apiDocs():
addView('docs', apiDocs) addView('docs', apiDocs)
# Make non basic auth option to get api key # Make non basic auth option to get api key
class KeyHandler(RequestHandler): class KeyHandler(RequestHandler):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
api = None api_key = None
try: try:
username = Env.setting('username') username = Env.setting('username')
password = Env.setting('password') password = Env.setting('password')
if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password): if (self.get_argument('u') == md5(username) or not username) and (self.get_argument('p') == password or not password):
api = Env.setting('api_key') api_key = Env.setting('api_key')
self.write({ self.write({
'success': api is not None, 'success': api_key is not None,
'api_key': api 'api_key': api_key
}) })
except: except:
log.error('Failed doing key request: %s', (traceback.format_exc())) log.error('Failed doing key request: %s', (traceback.format_exc()))
@@ -102,20 +103,21 @@ class LoginHandler(BaseHandler):
def post(self, *args, **kwargs): def post(self, *args, **kwargs):
api = None api_key = None
username = Env.setting('username') username = Env.setting('username')
password = Env.setting('password') password = Env.setting('password')
if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password): if (self.get_argument('username') == username or not username) and (md5(self.get_argument('password')) == password or not password):
api = Env.setting('api_key') api_key = Env.setting('api_key')
if api: if api_key:
remember_me = tryInt(self.get_argument('remember_me', default = 0)) remember_me = tryInt(self.get_argument('remember_me', default = 0))
self.set_secure_cookie('user', api, expires_days = 30 if remember_me > 0 else None) self.set_secure_cookie('user', api_key, expires_days = 30 if remember_me > 0 else None)
self.redirect(Env.get('web_base')) self.redirect(Env.get('web_base'))
class LogoutHandler(BaseHandler): class LogoutHandler(BaseHandler):
def get(self, *args, **kwargs): def get(self, *args, **kwargs):
@@ -136,4 +138,3 @@ def page_not_found(rh):
rh.set_status(404) rh.set_status(404)
rh.write('Wrong API key used') rh.write('Wrong API key used')
+8 -2
View File
@@ -20,6 +20,7 @@ api_nonblock = {}
api_docs = {} api_docs = {}
api_docs_missing = [] api_docs_missing = []
def run_async(func): def run_async(func):
@wraps(func) @wraps(func)
def async_func(*args, **kwargs): def async_func(*args, **kwargs):
@@ -29,6 +30,7 @@ def run_async(func):
return async_func return async_func
# NonBlock API handler # NonBlock API handler
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
@@ -61,6 +63,7 @@ class NonBlockHandler(RequestHandler):
self.stopper = None self.stopper = None
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs): def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple api_nonblock[route] = func_tuple
@@ -69,6 +72,7 @@ def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
# Blocking API handler # Blocking API handler
class ApiHandler(RequestHandler): class ApiHandler(RequestHandler):
@@ -98,11 +102,12 @@ class ApiHandler(RequestHandler):
@run_async @run_async
def run_handler(callback): def run_handler(callback):
try: try:
result = api[route](**kwargs) res = api[route](**kwargs)
callback(result) callback(res)
except: except:
log.error('Failed doing api request "%s": %s', (route, traceback.format_exc())) log.error('Failed doing api request "%s": %s', (route, traceback.format_exc()))
callback({'success': False, 'error': 'Failed returning results'}) callback({'success': False, 'error': 'Failed returning results'})
result = yield tornado.gen.Task(run_handler) result = yield tornado.gen.Task(run_handler)
# Check JSONP callback # Check JSONP callback
@@ -122,6 +127,7 @@ class ApiHandler(RequestHandler):
api_locks[route].release() api_locks[route].release()
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):
if static: func(route) if static: func(route)
+1
View File
@@ -1,6 +1,7 @@
from .main import Core from .main import Core
from uuid import uuid4 from uuid import uuid4
def start(): def start():
return Core() return Core()
+5 -1
View File
@@ -55,6 +55,10 @@ class Core(Plugin):
if not Env.get('desktop'): if not Env.get('desktop'):
self.signalHandler() self.signalHandler()
# Set default urlopen timeout
import socket
socket.setdefaulttimeout(30)
def md5Password(self, value): def md5Password(self, value):
return md5(value) if value else '' return md5(value) if value else ''
@@ -113,7 +117,7 @@ class Core(Plugin):
if len(still_running) == 0: if len(still_running) == 0:
break break
elif starttime < time.time() - 30: # Always force break after 30s wait elif starttime < time.time() - 30: # Always force break after 30s wait
break break
running = list(set(still_running) - set(self.ignore_restart)) running = list(set(still_running) - set(self.ignore_restart))
@@ -1,5 +1,6 @@
from .main import ClientScript from .main import ClientScript
def start(): def start():
return ClientScript() return ClientScript()
+6 -3
View File
@@ -34,6 +34,8 @@ class ClientScript(Plugin):
'scripts/library/question.js', 'scripts/library/question.js',
'scripts/library/scrollspy.js', 'scripts/library/scrollspy.js',
'scripts/library/spin.js', 'scripts/library/spin.js',
'scripts/library/Array.stableSort.js',
'scripts/library/async.js',
'scripts/couchpotato.js', 'scripts/couchpotato.js',
'scripts/api.js', 'scripts/api.js',
'scripts/library/history.js', 'scripts/library/history.js',
@@ -47,13 +49,14 @@ class ClientScript(Plugin):
'scripts/page/settings.js', 'scripts/page/settings.js',
'scripts/page/about.js', 'scripts/page/about.js',
'scripts/page/manage.js', 'scripts/page/manage.js',
'scripts/misc/downloaders.js',
], ],
} }
urls = {'style': {}, 'script': {}, } urls = {'style': {}, 'script': {}}
minified = {'style': {}, 'script': {}, } minified = {'style': {}, 'script': {}}
paths = {'style': {}, 'script': {}, } paths = {'style': {}, 'script': {}}
comment = { comment = {
'style': '/*** %s:%d ***/\n', 'style': '/*** %s:%d ***/\n',
'script': '// %s:%d\n' 'script': '// %s:%d\n'
@@ -1,5 +1,6 @@
from .main import Desktop from .main import Desktop
def start(): def start():
return Desktop() return Desktop()
@@ -1,5 +1,6 @@
from .main import Scheduler from .main import Scheduler
def start(): def start():
return Scheduler() return Scheduler()
+14 -2
View File
@@ -17,6 +17,7 @@ class Scheduler(Plugin):
addEvent('schedule.cron', self.cron) addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval) addEvent('schedule.interval', self.interval)
addEvent('schedule.remove', self.remove) addEvent('schedule.remove', self.remove)
addEvent('schedule.queue', self.queue)
self.sched = Sched(misfire_grace_time = 60) self.sched = Sched(misfire_grace_time = 60)
self.sched.start() self.sched.start()
@@ -31,13 +32,13 @@ class Scheduler(Plugin):
pass pass
def doShutdown(self): def doShutdown(self):
super(Scheduler, self).doShutdown()
self.stop() self.stop()
return super(Scheduler, self).doShutdown()
def stop(self): def stop(self):
if self.started: if self.started:
log.debug('Stopping scheduler') log.debug('Stopping scheduler')
self.sched.shutdown() self.sched.shutdown(wait = False)
log.debug('Scheduler stopped') log.debug('Scheduler stopped')
self.started = False self.started = False
@@ -64,3 +65,14 @@ class Scheduler(Plugin):
'seconds': seconds, 'seconds': seconds,
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds) 'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
} }
def queue(self, handlers = None):
if not handlers: handlers = []
for h in handlers:
h()
if self.shuttingDown():
break
return True
@@ -2,6 +2,7 @@ from .main import Updater
from couchpotato.environment import Env from couchpotato.environment import Env
import os import os
def start(): def start():
return Updater() return Updater()
+24 -18
View File
@@ -15,6 +15,7 @@ import time
import traceback import traceback
import version import version
import zipfile import zipfile
from six.moves import filter
log = CPLog(__name__) log = CPLog(__name__)
@@ -32,6 +33,7 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
addEvent('app.load', self.logVersion, priority = 10000)
addEvent('app.load', self.setCrons) addEvent('app.load', self.setCrons)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
@@ -53,12 +55,16 @@ class Updater(Plugin):
addEvent('setting.save.updater.enabled.after', self.setCrons) addEvent('setting.save.updater.enabled.after', self.setCrons)
def logVersion(self):
info = self.info()
log.info('=== VERSION %s, using %s ===', (info.get('version', {}).get('repr', 'UNKNOWN'), self.updater.getName()))
def setCrons(self): def setCrons(self):
fireEvent('schedule.remove', 'updater.check', single = True) fireEvent('schedule.remove', 'updater.check', single = True)
if self.isEnabled(): if self.isEnabled():
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6) fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
self.autoUpdate() # Check after enabling self.autoUpdate() # Check after enabling
def autoUpdate(self): def autoUpdate(self):
if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed: if self.isEnabled() and self.check() and self.conf('automatic') and not self.updater.update_failed:
@@ -146,6 +152,9 @@ class BaseUpdater(Plugin):
'branch': self.branch, 'branch': self.branch,
} }
def getVersion(self):
pass
def check(self): def check(self):
pass pass
@@ -174,7 +183,6 @@ class BaseUpdater(Plugin):
log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc())) log.error('Couldn\'t remove empty directory %s: %s', (full_path, traceback.format_exc()))
class GitUpdater(BaseUpdater): class GitUpdater(BaseUpdater):
def __init__(self, git_command): def __init__(self, git_command):
@@ -183,9 +191,6 @@ class GitUpdater(BaseUpdater):
def doUpdate(self): def doUpdate(self):
try: try:
log.debug('Stashing local changes')
self.repo.saveStash()
log.info('Updating to latest version') log.info('Updating to latest version')
self.repo.pull() self.repo.pull()
@@ -204,14 +209,15 @@ class GitUpdater(BaseUpdater):
if not self.version: if not self.version:
try: try:
output = self.repo.getHead() # Yes, please output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s', output.hash) log.debug('Git version output: %s', output.hash)
self.version = { self.version = {
'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
'hash': output.hash[:8], 'hash': output.hash[:8],
'date': output.getDate(), 'date': output.getDate(),
'type': 'git', 'type': 'git',
} }
except Exception, e: except Exception as e:
log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e) log.error('Failed using GIT updater, running from source, you need to have GIT installed. %s', e)
return 'No GIT' return 'No GIT'
@@ -234,7 +240,7 @@ class GitUpdater(BaseUpdater):
local = self.repo.getHead() local = self.repo.getHead()
remote = branch.getHead() remote = branch.getHead()
log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8])) log.debug('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate(): if local.getDate() < remote.getDate():
self.update_version = { self.update_version = {
@@ -247,7 +253,6 @@ class GitUpdater(BaseUpdater):
return False return False
class SourceUpdater(BaseUpdater): class SourceUpdater(BaseUpdater):
def __init__(self): def __init__(self):
@@ -273,9 +278,9 @@ class SourceUpdater(BaseUpdater):
# Extract # Extract
if download_data.get('type') == 'zip': if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination) zip_file = zipfile.ZipFile(destination)
zip.extractall(extracted_path) zip_file.extractall(extracted_path)
zip.close() zip_file.close()
else: else:
tar = tarfile.open(destination) tar = tarfile.open(destination)
tar.extractall(path = extracted_path) tar.extractall(path = extracted_path)
@@ -342,13 +347,12 @@ class SourceUpdater(BaseUpdater):
return True return True
def removeDir(self, path): def removeDir(self, path):
try: try:
if os.path.isdir(path): if os.path.isdir(path):
shutil.rmtree(path) shutil.rmtree(path)
except OSError, inst: except OSError as inst:
os.chmod(inst.filename, 0777) os.chmod(inst.filename, 0o777)
self.removeDir(path) self.removeDir(path)
def getVersion(self): def getVersion(self):
@@ -362,7 +366,8 @@ class SourceUpdater(BaseUpdater):
log.debug('Source version output: %s', output) log.debug('Source version output: %s', output)
self.version = output self.version = output
self.version['type'] = 'source' self.version['type'] = 'source'
except Exception, e: self.version['repr'] = 'source:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.get('hash', '')[:8], datetime.fromtimestamp(output.get('date', 0)))
except Exception as e:
log.error('Failed using source updater. %s', e) log.error('Failed using source updater. %s', e)
return {} return {}
@@ -392,7 +397,7 @@ class SourceUpdater(BaseUpdater):
return { return {
'hash': commit['sha'], 'hash': commit['sha'],
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())), 'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
} }
except: except:
log.error('Failed getting latest request from github: %s', traceback.format_exc()) log.error('Failed getting latest request from github: %s', traceback.format_exc())
@@ -437,7 +442,7 @@ class DesktopUpdater(BaseUpdater):
if latest and latest != current_version.get('hash'): if latest and latest != current_version.get('hash'):
self.update_version = { self.update_version = {
'hash': latest, 'hash': latest,
'date': None, 'date': None,
'changelog': self.desktop._changelogURL, 'changelog': self.desktop._changelogURL,
} }
@@ -449,6 +454,7 @@ class DesktopUpdater(BaseUpdater):
def getVersion(self): def getVersion(self):
return { return {
'repr': 'desktop: %s' % self.desktop._esky.active_version,
'hash': self.desktop._esky.active_version, 'hash': self.desktop._esky.active_version,
'date': None, 'date': None,
'type': 'desktop', 'type': 'desktop',
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate(); self.doUpdate();
else { else {
App.unBlockPage(); App.unBlockPage();
App.fireEvent('message', 'No updates available'); App.trigger('message', ['No updates available']);
} }
} }
}) })
+25 -7
View File
@@ -1,4 +1,5 @@
from base64 import b32decode, b16encode from base64 import b32decode, b16encode
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -13,6 +14,7 @@ class Downloader(Provider):
protocol = [] protocol = []
http_time_between_calls = 0 http_time_between_calls = 0
status_support = True
torrent_sources = [ torrent_sources = [
'http://torrage.com/torrent/%s.torrent', 'http://torrage.com/torrent/%s.torrent',
@@ -41,6 +43,7 @@ class Downloader(Provider):
addEvent('download.remove_failed', self._removeFailed) addEvent('download.remove_failed', self._removeFailed)
addEvent('download.pause', self._pause) addEvent('download.pause', self._pause)
addEvent('download.process_complete', self._processComplete) addEvent('download.process_complete', self._processComplete)
addApiView('download.%s.test' % self.getName().lower(), self._test)
def getEnabledProtocol(self): def getEnabledProtocol(self):
for download_protocol in self.protocol: for download_protocol in self.protocol:
@@ -49,22 +52,27 @@ class Downloader(Provider):
return [] return []
def _download(self, data = None, movie = None, manual = False, filedata = None): def _download(self, data = None, media = None, manual = False, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
if self.isDisabled(manual, data): if self.isDisabled(manual, data):
return return
return self.download(data = data, movie = movie, filedata = filedata) return self.download(data = data, media = media, filedata = filedata)
def _getAllDownloadStatus(self): def _getAllDownloadStatus(self, download_ids):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
return self.getAllDownloadStatus() ids = [download_id['id'] for download_id in download_ids if download_id['downloader'] == self.getName()]
def getAllDownloadStatus(self): if ids:
return return self.getAllDownloadStatus(ids)
else:
return
def getAllDownloadStatus(self, ids):
return []
def _removeFailed(self, release_download): def _removeFailed(self, release_download):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
@@ -128,6 +136,7 @@ class Downloader(Provider):
def downloadReturnId(self, download_id): def downloadReturnId(self, download_id):
return { return {
'downloader': self.getName(), 'downloader': self.getName(),
'status_support': self.status_support,
'id': download_id 'id': download_id
} }
@@ -151,6 +160,15 @@ class Downloader(Provider):
(d_manual and manual or d_manual is False) and \ (d_manual and manual or d_manual is False) and \
(not data or self.isCorrectProtocol(data.get('protocol'))) (not data or self.isCorrectProtocol(data.get('protocol')))
def _test(self):
t = self.test()
if isinstance(t, tuple):
return {'success': t[0], 'msg': t[1]}
return {'success': t}
def test(self):
return False
def _pause(self, release_download, pause = True): def _pause(self, release_download, pause = True):
if self.isDisabled(manual = True, data = {}): if self.isDisabled(manual = True, data = {}):
return return
@@ -1,6 +1,7 @@
from .main import Blackhole from .main import Blackhole
from couchpotato.core.helpers.variable import getDownloadDir from couchpotato.core.helpers.variable import getDownloadDir
def start(): def start():
return Blackhole() return Blackhole()
@@ -13,7 +14,7 @@ config = [{
'list': 'download_providers', 'list': 'download_providers',
'name': 'blackhole', 'name': 'blackhole',
'label': 'Black hole', 'label': 'Black hole',
'description': 'Download the NZB/Torrent to a specific folder.', 'description': 'Download the NZB/Torrent to a specific folder. <em>Note: Seeding and copying/linking features do <strong>not</strong> work with Black hole</em>.',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
+21 -5
View File
@@ -1,5 +1,6 @@
from __future__ import with_statement from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
import os import os
@@ -11,9 +12,10 @@ log = CPLog(__name__)
class Blackhole(Downloader): class Blackhole(Downloader):
protocol = ['nzb', 'torrent', 'torrent_magnet'] protocol = ['nzb', 'torrent', 'torrent_magnet']
status_support = False
def download(self, data = None, movie = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
@@ -33,7 +35,7 @@ class Blackhole(Downloader):
log.error('No nzb/torrent available: %s', data.get('url')) log.error('No nzb/torrent available: %s', data.get('url'))
return False return False
file_name = self.createFileName(data, filedata, movie) file_name = self.createFileName(data, filedata, media)
full_path = os.path.join(directory, file_name) full_path = os.path.join(directory, file_name)
if self.conf('create_subdir'): if self.conf('create_subdir'):
@@ -51,10 +53,10 @@ class Blackhole(Downloader):
with open(full_path, 'wb') as f: with open(full_path, 'wb') as f:
f.write(filedata) f.write(filedata)
os.chmod(full_path, Env.getPermission('file')) os.chmod(full_path, Env.getPermission('file'))
return True return self.downloadReturnId('')
else: else:
log.info('File %s already exists.', full_path) log.info('File %s already exists.', full_path)
return True return self.downloadReturnId('')
except: except:
log.error('Failed to download to blackhole %s', traceback.format_exc()) log.error('Failed to download to blackhole %s', traceback.format_exc())
@@ -66,6 +68,20 @@ class Blackhole(Downloader):
return False return False
def test(self):
directory = self.conf('directory')
if directory and os.path.isdir(directory):
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
# Check if folder is writable
self.createFile(test_file, 'This is a test file')
if os.path.isfile(test_file):
os.remove(test_file)
return True
return False
def getEnabledProtocol(self): def getEnabledProtocol(self):
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
return super(Blackhole, self).getEnabledProtocol() return super(Blackhole, self).getEnabledProtocol()
@@ -1,5 +1,6 @@
from .main import Deluge from .main import Deluge
def start(): def start():
return Deluge() return Deluge()
+40 -19
View File
@@ -2,7 +2,7 @@ from base64 import b64encode, b16encode, b32decode
from bencode import bencode as benc, bdecode from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryFloat from couchpotato.core.helpers.variable import tryFloat, cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
@@ -20,19 +20,22 @@ class Deluge(Downloader):
log = CPLog(__name__) log = CPLog(__name__)
drpc = None drpc = None
def connect(self): def connect(self, reconnect = False):
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if not self.drpc: if not self.drpc or reconnect:
self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password')) self.drpc = DelugeRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return self.drpc return self.drpc
def download(self, data, movie, filedata = None): def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol'))) log.info('Sending "%s" (%s) to Deluge.', (data.get('name'), data.get('protocol')))
if not self.connect(): if not self.connect():
@@ -73,7 +76,7 @@ class Deluge(Downloader):
if data.get('protocol') == 'torrent_magnet': if data.get('protocol') == 'torrent_magnet':
remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options) remote_torrent = self.drpc.add_torrent_magnet(data.get('url'), options)
else: else:
filename = self.createFileName(data, filedata, movie) filename = self.createFileName(data, filedata, media)
remote_torrent = self.drpc.add_torrent_file(filename, filedata, options) remote_torrent = self.drpc.add_torrent_file(filename, filedata, options)
if not remote_torrent: if not remote_torrent:
@@ -83,24 +86,34 @@ class Deluge(Downloader):
log.info('Torrent sent to Deluge successfully.') log.info('Torrent sent to Deluge successfully.')
return self.downloadReturnId(remote_torrent) return self.downloadReturnId(remote_torrent)
def getAllDownloadStatus(self): def test(self):
if self.connect(True) and self.drpc.test():
return True
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking Deluge download status.') log.debug('Checking Deluge download status.')
if not self.connect(): if not self.connect():
return False return []
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
queue = self.drpc.get_alltorrents() queue = self.drpc.get_alltorrents(ids)
if not queue: if not queue:
log.debug('Nothing in queue or error') log.debug('Nothing in queue or error')
return False return []
for torrent_id in queue: for torrent_id in queue:
torrent = queue[torrent_id] torrent = queue[torrent_id]
log.debug('name=%s / id=%s / save_path=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
if not 'hash' in torrent:
# When given a list of ids, deluge will return an empty item for a non-existant torrent.
continue
log.debug('name=%s / id=%s / save_path=%s / move_on_completed=%s / move_completed_path=%s / hash=%s / progress=%s / state=%s / eta=%s / ratio=%s / stop_ratio=%s / is_seed=%s / is_finished=%s / paused=%s', (torrent['name'], torrent['hash'], torrent['save_path'], torrent['move_on_completed'], torrent['move_completed_path'], torrent['hash'], torrent['progress'], torrent['state'], torrent['eta'], torrent['ratio'], torrent['stop_ratio'], torrent['is_seed'], torrent['is_finished'], torrent['paused']))
# Deluge has no easy way to work out if a torrent is stalled or failing. # Deluge has no easy way to work out if a torrent is stalled or failing.
#status = 'failed' #status = 'failed'
@@ -149,6 +162,7 @@ class Deluge(Downloader):
log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else '')) log.debug('Requesting Deluge to remove the torrent %s%s.', (release_download['name'], ' and cleanup the downloaded files' if delete_files else ''))
return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files) return self.drpc.remove_torrent(release_download['id'], remove_local_data = delete_files)
class DelugeRPC(object): class DelugeRPC(object):
host = 'localhost' host = 'localhost'
@@ -169,6 +183,13 @@ class DelugeRPC(object):
self.client = DelugeClient() self.client = DelugeClient()
self.client.connect(self.host, int(self.port), self.username, self.password) self.client.connect(self.host, int(self.port), self.username, self.password)
def test(self):
try:
self.connect()
except:
return False
return True
def add_torrent_magnet(self, torrent, options): def add_torrent_magnet(self, torrent, options):
torrent_id = False torrent_id = False
try: try:
@@ -179,7 +200,7 @@ class DelugeRPC(object):
if torrent_id and options['label']: if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err: except Exception as err:
log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc())) log.error('Failed to add torrent magnet %s: %s %s', (torrent, err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -197,7 +218,7 @@ class DelugeRPC(object):
if torrent_id and options['label']: if torrent_id and options['label']:
self.client.label.set_torrent(torrent_id, options['label']).get() self.client.label.set_torrent(torrent_id, options['label']).get()
except Exception, err: except Exception as err:
log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc())) log.error('Failed to add torrent file %s: %s %s', (filename, err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -205,12 +226,12 @@ class DelugeRPC(object):
return torrent_id return torrent_id
def get_alltorrents(self): def get_alltorrents(self, ids):
ret = False ret = False
try: try:
self.connect() self.connect()
ret = self.client.core.get_torrents_status({}, {}).get() ret = self.client.core.get_torrents_status({'id': ids}, ('name', 'hash', 'save_path', 'move_completed_path', 'progress', 'state', 'eta', 'ratio', 'stop_ratio', 'is_seed', 'is_finished', 'paused', 'move_on_completed', 'files')).get()
except Exception, err: except Exception as err:
log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc())) log.error('Failed to get all torrents: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -221,7 +242,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
self.client.core.pause_torrent(torrent_ids).get() self.client.core.pause_torrent(torrent_ids).get()
except Exception, err: except Exception as err:
log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to pause torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -231,7 +252,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
self.client.core.resume_torrent(torrent_ids).get() self.client.core.resume_torrent(torrent_ids).get()
except Exception, err: except Exception as err:
log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to resume torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -242,7 +263,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get() ret = self.client.core.remove_torrent(torrent_id, remove_local_data).get()
except Exception, err: except Exception as err:
log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc())) log.error('Failed to remove torrent: %s %s', (err, traceback.format_exc()))
finally: finally:
if self.client: if self.client:
@@ -1,5 +1,6 @@
from .main import NZBGet from .main import NZBGet
def start(): def start():
return NZBGet() return NZBGet()
@@ -25,6 +26,13 @@ config = [{
'default': 'localhost:6789', 'default': 'localhost:6789',
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>', 'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
}, },
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{ {
'name': 'username', 'name': 'username',
'default': 'nzbget', 'default': 'nzbget',
+75 -51
View File
@@ -1,7 +1,7 @@
from base64 import standard_b64encode from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import ss, sp from couchpotato.core.helpers.encoding import ss, sp
from couchpotato.core.helpers.variable import tryInt, md5 from couchpotato.core.helpers.variable import tryInt, md5, cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
import re import re
@@ -16,11 +16,10 @@ log = CPLog(__name__)
class NZBGet(Downloader): class NZBGet(Downloader):
protocol = ['nzb'] protocol = ['nzb']
rpc = 'xmlrpc'
url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc' def download(self, data = None, media = None, filedata = None):
if not media: media = {}
def download(self, data = None, movie = None, filedata = None):
if not movie: movie = {}
if not data: data = {} if not data: data = {}
if not filedata: if not filedata:
@@ -29,10 +28,10 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name')) log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} nzb_name = ss('%s.nzb' % self.createNzbName(data, media))
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = self.getRPC()
rpc = xmlrpclib.ServerProxy(url)
try: try:
if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name): if rpc.writelog('INFO', 'CouchPotato connected to drop off %s.' % nzb_name):
log.debug('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
@@ -41,7 +40,7 @@ class NZBGet(Downloader):
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False return False
except xmlrpclib.ProtocolError, e: except xmlrpclib.ProtocolError as e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
@@ -55,7 +54,7 @@ class NZBGet(Downloader):
if xml_response: if xml_response:
log.info('NZB sent successfully to NZBGet') log.info('NZB sent successfully to NZBGet')
nzb_id = md5(data['url']) # about as unique as they come ;) nzb_id = md5(data['url']) # about as unique as they come ;)
couchpotato_id = "couchpotato=" + nzb_id couchpotato_id = "couchpotato=" + nzb_id
groups = rpc.listgroups() groups = rpc.listgroups()
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name] file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
@@ -67,13 +66,32 @@ class NZBGet(Downloader):
log.error('NZBGet could not add %s to the queue.', nzb_name) log.error('NZBGet could not add %s to the queue.', nzb_name)
return False return False
def getAllDownloadStatus(self): def test(self):
rpc = self.getRPC()
try:
if rpc.writelog('INFO', 'CouchPotato connected to test connection'):
log.debug('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError as e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
return True
def getAllDownloadStatus(self, ids):
log.debug('Checking NZBGet download status.') log.debug('Checking NZBGet download status.')
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} rpc = self.getRPC()
rpc = xmlrpclib.ServerProxy(url)
try: try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'): if rpc.writelog('INFO', 'CouchPotato connected to check status'):
log.debug('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
@@ -81,13 +99,13 @@ class NZBGet(Downloader):
log.info('Successfully connected to NZBGet, but unable to send a message') log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False return []
except xmlrpclib.ProtocolError, e: except xmlrpclib.ProtocolError as e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
log.error('Protocol Error: %s', e) log.error('Protocol Error: %s', e)
return False return []
# Get NZBGet data # Get NZBGet data
try: try:
@@ -97,56 +115,59 @@ class NZBGet(Downloader):
history = rpc.history() history = rpc.history()
except: except:
log.error('Failed getting data: %s', traceback.format_exc(1)) log.error('Failed getting data: %s', traceback.format_exc(1))
return False return []
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
for nzb in groups: for nzb in groups:
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
try: try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = nzb['NZBID'] nzb_id = nzb['NZBID']
if nzb_id in ids:
log.debug('Found %s in NZBGet download queue', nzb['NZBFilename'])
timeleft = -1
try:
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']):
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except:
pass
timeleft = -1 release_downloads.append({
try: 'id': nzb_id,
if nzb['ActiveDownloads'] > 0 and nzb['DownloadRate'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']): 'name': nzb['NZBFilename'],
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) 'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
except: # Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
pass 'timeleft': timeleft,
})
release_downloads.append({
'id': nzb_id,
'name': nzb['NZBFilename'],
'original_status': 'DOWNLOADING' if nzb['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': timeleft,
})
for nzb in queue: # 'Parameters' is not passed in rpc.postqueue for nzb in queue: # 'Parameters' is not passed in rpc.postqueue
log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename']) if nzb['NZBID'] in ids:
release_downloads.append({ log.debug('Found %s in NZBGet postprocessing queue', nzb['NZBFilename'])
'id': nzb['NZBID'], release_downloads.append({
'name': nzb['NZBFilename'], 'id': nzb['NZBID'],
'original_status': nzb['Stage'], 'name': nzb['NZBFilename'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1, 'original_status': nzb['Stage'],
}) 'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
for nzb in history: for nzb in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
try: try:
nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0] nzb_id = [param['Value'] for param in nzb['Parameters'] if param['Name'] == 'couchpotato'][0]
except: except:
nzb_id = nzb['NZBID'] nzb_id = nzb['NZBID']
release_downloads.append({
'id': nzb_id, if nzb_id in ids:
'name': nzb['NZBFilename'], log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (nzb['NZBFilename'] , nzb['ParStatus'], nzb['ScriptStatus'] , nzb['Log']))
'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed', release_downloads.append({
'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'], 'id': nzb_id,
'timeleft': str(timedelta(seconds = 0)), 'name': nzb['NZBFilename'],
'folder': sp(nzb['DestDir']) 'status': 'completed' if nzb['ParStatus'] in ['SUCCESS', 'NONE'] and nzb['ScriptStatus'] in ['SUCCESS', 'NONE'] else 'failed',
}) 'original_status': nzb['ParStatus'] + ', ' + nzb['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(nzb['DestDir'])
})
return release_downloads return release_downloads
@@ -154,9 +175,8 @@ class NZBGet(Downloader):
log.info('%s failed downloading, deleting...', release_download['name']) log.info('%s failed downloading, deleting...', release_download['name'])
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')} rpc = self.getRPC()
rpc = xmlrpclib.ServerProxy(url)
try: try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'): if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
log.debug('Successfully connected to NZBGet') log.debug('Successfully connected to NZBGet')
@@ -165,7 +185,7 @@ class NZBGet(Downloader):
except socket.error: except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.') log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False return False
except xmlrpclib.ProtocolError, e: except xmlrpclib.ProtocolError as e:
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
@@ -190,3 +210,7 @@ class NZBGet(Downloader):
return False return False
return True return True
def getRPC(self):
url = cleanHost(host = self.conf('host'), ssl = self.conf('ssl'), username = self.conf('username'), password = self.conf('password')) + self.rpc
return xmlrpclib.ServerProxy(url)
@@ -1,5 +1,6 @@
from .main import NZBVortex from .main import NZBVortex
def start(): def start():
return NZBVortex() return NZBVortex()
@@ -22,7 +23,15 @@ config = [{
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'https://localhost:4321', 'default': 'localhost:4321',
'description': 'Hostname with port. Usually <strong>localhost:4321</strong>',
},
{
'name': 'ssl',
'default': 1,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
}, },
{ {
'name': 'api_key', 'name': 'api_key',
+38 -28
View File
@@ -8,9 +8,11 @@ from uuid import uuid4
import hashlib import hashlib
import httplib import httplib
import json import json
import os
import socket import socket
import ssl import ssl
import sys import sys
import time
import traceback import traceback
import urllib2 import urllib2
@@ -23,44 +25,54 @@ class NZBVortex(Downloader):
api_level = None api_level = None
session_id = None session_id = None
def download(self, data = None, movie = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
# Send the nzb # Send the nzb
try: try:
nzb_filename = self.createFileName(data, filedata, movie) nzb_filename = self.createFileName(data, filedata, media)
self.call('nzb/add', params = {'file': (nzb_filename, filedata)}, multipart = True) self.call('nzb/add', files = {'file': (nzb_filename, filedata)})
time.sleep(10)
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if nzb['name'] == nzb_filename][0] nzb_id = [nzb['id'] for nzb in raw_statuses.get('nzbs', []) if os.path.basename(nzb['nzbFileName']) == nzb_filename][0]
return self.downloadReturnId(nzb_id) return self.downloadReturnId(nzb_id)
except: except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False return False
def getAllDownloadStatus(self): def test(self):
try:
login_result = self.login()
except:
return False
return login_result
def getAllDownloadStatus(self, ids):
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
for nzb in raw_statuses.get('nzbs', []): for nzb in raw_statuses.get('nzbs', []):
if nzb['id'] in ids:
# Check status # Check status
status = 'busy' status = 'busy'
if nzb['state'] == 20: if nzb['state'] == 20:
status = 'completed' status = 'completed'
elif nzb['state'] in [21, 22, 24]: elif nzb['state'] in [21, 22, 24]:
status = 'failed' status = 'failed'
release_downloads.append({ release_downloads.append({
'id': nzb['id'], 'id': nzb['id'],
'name': nzb['uiTitle'], 'name': nzb['uiTitle'],
'status': status, 'status': status,
'original_status': nzb['state'], 'original_status': nzb['state'],
'timeleft':-1, 'timeleft': -1,
'folder': sp(nzb['destinationPath']), 'folder': sp(nzb['destinationPath']),
}) })
return release_downloads return release_downloads
@@ -98,7 +110,6 @@ class NZBVortex(Downloader):
log.error('Login failed, please check you api-key') log.error('Login failed, please check you api-key')
return False return False
def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs): def call(self, call, parameters = None, repeat = False, auth = True, *args, **kwargs):
# Login first # Login first
@@ -112,15 +123,14 @@ class NZBVortex(Downloader):
params = tryUrlencode(parameters) params = tryUrlencode(parameters)
url = cleanHost(self.conf('host')) + 'api/' + call url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api/' + call
url_opener = urllib2.build_opener(HTTPSHandler())
try: try:
data = self.urlopen('%s?%s' % (url, params), opener = url_opener, *args, **kwargs) data = self.urlopen('%s?%s' % (url, params), *args, **kwargs)
if data: if data:
return json.loads(data) return json.loads(data)
except URLError, e: except URLError as e:
if hasattr(e, 'code') and e.code == 403: if hasattr(e, 'code') and e.code == 403:
# Try login and do again # Try login and do again
if not repeat: if not repeat:
@@ -138,12 +148,11 @@ class NZBVortex(Downloader):
if not self.api_level: if not self.api_level:
url = cleanHost(self.conf('host')) + 'api/app/apilevel' url = cleanHost(self.conf('host')) + 'api/app/apilevel'
url_opener = urllib2.build_opener(HTTPSHandler())
try: try:
data = self.urlopen(url, opener = url_opener, show_error = False) data = self.urlopen(url, show_error = False)
self.api_level = float(json.loads(data).get('apilevel')) self.api_level = float(json.loads(data).get('apilevel'))
except URLError, e: except URLError as e:
if hasattr(e, 'code') and e.code == 403: if hasattr(e, 'code') and e.code == 403:
log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher') log.error('This version of NZBVortex isn\'t supported. Please update to 2.8.6 or higher')
else: else:
@@ -173,6 +182,7 @@ class HTTPSConnection(httplib.HTTPSConnection):
self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1) self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, ssl_version = ssl.PROTOCOL_TLSv1)
class HTTPSHandler(urllib2.HTTPSHandler): class HTTPSHandler(urllib2.HTTPSHandler):
def https_open(self, req): def https_open(self, req):
return self.do_open(HTTPSConnection, req) return self.do_open(HTTPSConnection, req)
@@ -1,5 +1,6 @@
from .main import Pneumatic from .main import Pneumatic
def start(): def start():
return Pneumatic() return Pneumatic()
+27 -11
View File
@@ -1,5 +1,6 @@
from __future__ import with_statement from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import os import os
import traceback import traceback
@@ -11,9 +12,10 @@ class Pneumatic(Downloader):
protocol = ['nzb'] protocol = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s' strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
status_support = False
def download(self, data = None, movie = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
directory = self.conf('directory') directory = self.conf('directory')
@@ -25,27 +27,27 @@ class Pneumatic(Downloader):
log.error('No nzb available!') log.error('No nzb available!')
return False return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie)) full_path = os.path.join(directory, self.createFileName(data, filedata, media))
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(full_path):
log.info('Downloading %s to %s.', (data.get('protocol'), fullPath)) log.info('Downloading %s to %s.', (data.get('protocol'), full_path))
with open(fullPath, 'wb') as f: with open(full_path, 'wb') as f:
f.write(filedata) f.write(filedata)
nzb_name = self.createNzbName(data, movie) nzb_name = self.createNzbName(data, media)
strm_path = os.path.join(directory, nzb_name) strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb') strm_file = open(strm_path + '.strm', 'wb')
strmContent = self.strm_syntax % (fullPath, nzb_name) strmContent = self.strm_syntax % (full_path, nzb_name)
strm_file.write(strmContent) strm_file.write(strmContent)
strm_file.close() strm_file.close()
return True return self.downloadReturnId('')
else: else:
log.info('File %s already exists.', fullPath) log.info('File %s already exists.', full_path)
return True return self.downloadReturnId('')
except: except:
log.error('Failed to download .strm: %s', traceback.format_exc()) log.error('Failed to download .strm: %s', traceback.format_exc())
@@ -55,3 +57,17 @@ class Pneumatic(Downloader):
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc())) log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False return False
return False return False
def test(self):
directory = self.conf('directory')
if directory and os.path.isdir(directory):
test_file = sp(os.path.join(directory, 'couchpotato_test.txt'))
# Check if folder is writable
self.createFile(test_file, 'This is a test file')
if os.path.isfile(test_file):
os.remove(test_file)
return True
return False
@@ -1,5 +1,6 @@
from .main import rTorrent from .main import rTorrent
def start(): def start():
return rTorrent() return rTorrent()
@@ -20,11 +21,32 @@ config = [{
'type': 'enabler', 'type': 'enabler',
'radio_group': 'torrent', 'radio_group': 'torrent',
}, },
# @RuudBurger: How do I migrate this?
# {
# 'name': 'url',
# 'default': 'http://localhost:80/RPC2',
# 'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> '
# 'or <strong>http://localhost:80/RPC2</strong>'
# },
{ {
'name': 'url', 'name': 'host',
'default': 'http://localhost:80/RPC2', 'default': 'localhost:80',
'description': 'XML-RPC Endpoint URI. Usually <strong>scgi://localhost:5000</strong> ' 'description': 'RPC Communication URI. Usually <strong>scgi://localhost:5000</strong>, '
'or <strong>http://localhost:80/RPC2</strong>' '<strong>httprpc://localhost/rutorrent</strong> or <strong>localhost:80</strong>'
},
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{
'name': 'rpc_url',
'type': 'string',
'default': 'RPC2',
'advanced': True,
'description': 'Change if your RPC mount is at a different path.',
}, },
{ {
'name': 'username', 'name': 'username',
@@ -58,14 +80,6 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Also remove the leftover files.', 'description': 'Also remove the leftover files.',
}, },
{
'name': 'append_label',
'label': 'Append Label',
'default': False,
'advanced': True,
'type': 'bool',
'description': 'Append label to download location. Requires you to set the download location above.',
},
{ {
'name': 'paused', 'name': 'paused',
'type': 'bool', 'type': 'bool',
+109 -44
View File
@@ -1,12 +1,15 @@
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.helpers.variable import cleanHost, splitString
from couchpotato.core.logger import CPLog
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from bencode import bencode, bdecode from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import sp
from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
from rtorrent import RTorrent from rtorrent import RTorrent
from rtorrent.err import MethodError from rtorrent.err import MethodError
from urlparse import urlparse
import os import os
log = CPLog(__name__) log = CPLog(__name__)
@@ -16,29 +19,75 @@ class rTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet'] protocol = ['torrent', 'torrent_magnet']
rt = None rt = None
error_msg = ''
def connect(self): # Migration url to host options
def __init__(self):
super(rTorrent, self).__init__()
addEvent('app.load', self.migrate)
addEvent('setting.save.rtorrent.*.after', self.settingsChanged)
def migrate(self):
url = self.conf('url')
if url:
host_split = splitString(url.split('://')[-1], split_on = '/')
self.conf('ssl', value = url.startswith('https'))
self.conf('host', value = host_split[0].strip())
self.conf('rpc_url', value = '/'.join(host_split[1:]))
self.deleteConf('url')
def settingsChanged(self):
# Reset active connection if settings have changed
if self.rt:
log.debug('Settings have changed, closing active connection')
self.rt = None
return True
def connect(self, reconnect = False):
# Already connected? # Already connected?
if self.rt is not None: if not reconnect and self.rt is not None:
return self.rt return self.rt
# Ensure url is set url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl'))
if not self.conf('url'): parsed = urlparse(url)
log.error('Config properties are not filled in correctly, url is missing.')
return False # rpc_url is only used on http/https scgi pass-through
if parsed.scheme in ['http', 'https']:
url += self.conf('rpc_url')
if self.conf('username') and self.conf('password'): if self.conf('username') and self.conf('password'):
self.rt = RTorrent( self.rt = RTorrent(
self.conf('url'), url,
self.conf('username'), self.conf('username'),
self.conf('password') self.conf('password')
) )
else: else:
self.rt = RTorrent(self.conf('url')) self.rt = RTorrent(url)
self.error_msg = ''
try:
self.rt._verify_conn()
except AssertionError as e:
self.error_msg = e.message
self.rt = None
return self.rt return self.rt
def _update_provider_group(self, name, data): def test(self):
if self.connect(True):
return True
if self.error_msg:
return False, 'Connection failed: ' + self.error_msg
return False
def updateProviderGroup(self, name, data):
if data.get('seed_time'): if data.get('seed_time'):
log.info('seeding time ignored, not supported') log.info('seeding time ignored, not supported')
@@ -70,28 +119,30 @@ class rTorrent(Downloader):
# Reset group action and disable it # Reset group action and disable it
group.set_command() group.set_command()
group.disable() group.disable()
except MethodError, err: except MethodError as err:
log.error('Unable to set group options: %s', err.msg) log.error('Unable to set group options: %s', err.msg)
return False return False
return True return True
def download(self, data, movie, filedata = None): def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.debug('Sending "%s" to rTorrent.', (data.get('name'))) log.debug('Sending "%s" to rTorrent.', (data.get('name')))
if not self.connect(): if not self.connect():
return False return False
group_name = 'cp_' + data.get('provider').lower() group_name = 'cp_' + data.get('provider').lower()
if not self._update_provider_group(group_name, data): if not self.updateProviderGroup(group_name, data):
return False return False
torrent_params = {} torrent_params = {}
if self.conf('label'): if self.conf('label'):
torrent_params['label'] = self.conf('label') torrent_params['label'] = self.conf('label')
if not filedata and data.get('protocol') == 'torrent': if not filedata and data.get('protocol') == 'torrent':
log.error('Failed sending torrent, no data') log.error('Failed sending torrent, no data')
return False return False
@@ -115,7 +166,7 @@ class rTorrent(Downloader):
# Send request to rTorrent # Send request to rTorrent
try: try:
# Send torrent to rTorrent # Send torrent to rTorrent
torrent = self.rt.load_torrent(filedata) torrent = self.rt.load_torrent(filedata, verify_retries=10)
if not torrent: if not torrent:
log.error('Unable to find the torrent, did it fail to load?') log.error('Unable to find the torrent, did it fail to load?')
@@ -125,9 +176,7 @@ class rTorrent(Downloader):
if self.conf('label'): if self.conf('label'):
torrent.set_custom(1, self.conf('label')) torrent.set_custom(1, self.conf('label'))
if self.conf('directory') and self.conf('append_label'): if self.conf('directory'):
torrent.set_directory(os.path.join(self.conf('directory'), self.conf('label')))
elif self.conf('directory'):
torrent.set_directory(self.conf('directory')) torrent.set_directory(self.conf('directory'))
# Set Ratio Group # Set Ratio Group
@@ -138,15 +187,30 @@ class rTorrent(Downloader):
torrent.start() torrent.start()
return self.downloadReturnId(torrent_hash) return self.downloadReturnId(torrent_hash)
except Exception, err: except Exception as err:
log.error('Failed to send torrent to rTorrent: %s', err) log.error('Failed to send torrent to rTorrent: %s', err)
return False return False
def getAllDownloadStatus(self): def getTorrentStatus(self, torrent):
if torrent.hashing or torrent.hash_checking or torrent.message:
return 'busy'
if not torrent.complete:
return 'busy'
if not torrent.open:
return 'completed'
if torrent.state and torrent.active:
return 'seeding'
return 'busy'
def getAllDownloadStatus(self, ids):
log.debug('Checking rTorrent download status.') log.debug('Checking rTorrent download status.')
if not self.connect(): if not self.connect():
return False return []
try: try:
torrents = self.rt.get_torrents() torrents = self.rt.get_torrents()
@@ -154,33 +218,34 @@ class rTorrent(Downloader):
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
for torrent in torrents: for torrent in torrents:
torrent_files = [] if torrent.info_hash in ids:
for file_item in torrent.get_files(): torrent_directory = os.path.normpath(torrent.directory)
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path))) torrent_files = []
status = 'busy' for file in torrent.get_files():
if torrent.complete: if not os.path.normpath(file.path).startswith(torrent_directory):
if torrent.active: file_path = os.path.join(torrent_directory, file.path.lstrip('/'))
status = 'seeding' else:
else: file_path = file.path
status = 'completed'
release_downloads.append({ torrent_files.append(sp(file_path))
'id': torrent.info_hash,
'name': torrent.name, release_downloads.append({
'status': status, 'id': torrent.info_hash,
'seed_ratio': torrent.ratio, 'name': torrent.name,
'original_status': torrent.state, 'status': self.getTorrentStatus(torrent),
'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1, 'seed_ratio': torrent.ratio,
'folder': sp(torrent.directory), 'original_status': torrent.state,
'files': '|'.join(torrent_files) 'timeleft': str(timedelta(seconds = float(torrent.left_bytes) / torrent.down_rate)) if torrent.down_rate > 0 else -1,
}) 'folder': sp(torrent.directory),
'files': '|'.join(torrent_files)
})
return release_downloads return release_downloads
except Exception, err: except Exception as err:
log.error('Failed to get status from rTorrent: %s', err) log.error('Failed to get status from rTorrent: %s', err)
return False return []
def pause(self, release_download, pause = True): def pause(self, release_download, pause = True):
if not self.connect(): if not self.connect():
@@ -1,5 +1,6 @@
from .main import Sabnzbd from .main import Sabnzbd
def start(): def start():
return Sabnzbd() return Sabnzbd()
@@ -24,6 +25,13 @@ config = [{
'name': 'host', 'name': 'host',
'default': 'localhost:8080', 'default': 'localhost:8080',
}, },
{
'name': 'ssl',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Use HyperText Transfer Protocol Secure, or <strong>https</strong>',
},
{ {
'name': 'api_key', 'name': 'api_key',
'label': 'Api Key', 'label': 'Api Key',
+56 -35
View File
@@ -16,8 +16,8 @@ class Sabnzbd(Downloader):
protocol = ['nzb'] protocol = ['nzb']
def download(self, data = None, movie = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
log.info('Sending "%s" to SABnzbd.', data.get('name')) log.info('Sending "%s" to SABnzbd.', data.get('name'))
@@ -25,7 +25,7 @@ class Sabnzbd(Downloader):
req_params = { req_params = {
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addurl',
'nzbname': self.createNzbName(data, movie), 'nzbname': self.createNzbName(data, media),
'priority': self.conf('priority'), 'priority': self.conf('priority'),
} }
@@ -36,14 +36,14 @@ class Sabnzbd(Downloader):
return False return False
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
nzb_filename = self.createFileName(data, filedata, movie) nzb_filename = self.createFileName(data, filedata, media)
req_params['mode'] = 'addfile' req_params['mode'] = 'addfile'
else: else:
req_params['name'] = data.get('url') req_params['name'] = data.get('url')
try: try:
if nzb_filename and req_params.get('mode') is 'addfile': if nzb_filename and req_params.get('mode') is 'addfile':
sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True) sab_data = self.call(req_params, files = {'nzbfile': (ss(nzb_filename), filedata)})
else: else:
sab_data = self.call(req_params) sab_data = self.call(req_params)
except URLError: except URLError:
@@ -64,7 +64,27 @@ class Sabnzbd(Downloader):
log.error('Error getting data from SABNZBd: %s', sab_data) log.error('Error getting data from SABNZBd: %s', sab_data)
return False return False
def getAllDownloadStatus(self): def test(self):
try:
sab_data = self.call({
'mode': 'version',
})
v = sab_data.split('.')
if int(v[0]) == 0 and int(v[1]) < 7:
return False, 'Your Sabnzbd client is too old, please update to newest version.'
# the version check will work even with wrong api key, so we need the next check as well
sab_data = self.call({
'mode': 'qstatus',
})
if not sab_data:
return False
except:
return False
return True
def getAllDownloadStatus(self, ids):
log.debug('Checking SABnzbd download status.') log.debug('Checking SABnzbd download status.')
@@ -75,7 +95,7 @@ class Sabnzbd(Downloader):
}) })
except: except:
log.error('Failed getting queue: %s', traceback.format_exc(1)) log.error('Failed getting queue: %s', traceback.format_exc(1))
return False return []
# Go through history items # Go through history items
try: try:
@@ -85,41 +105,42 @@ class Sabnzbd(Downloader):
}) })
except: except:
log.error('Failed getting history json: %s', traceback.format_exc(1)) log.error('Failed getting history json: %s', traceback.format_exc(1))
return False return []
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
# Get busy releases # Get busy releases
for nzb in queue.get('slots', []): for nzb in queue.get('slots', []):
status = 'busy' if nzb['nzo_id'] in ids:
if 'ENCRYPTED / ' in nzb['filename']: status = 'busy'
status = 'failed' if 'ENCRYPTED / ' in nzb['filename']:
status = 'failed'
release_downloads.append({ release_downloads.append({
'id': nzb['nzo_id'], 'id': nzb['nzo_id'],
'name': nzb['filename'], 'name': nzb['filename'],
'status': status, 'status': status,
'original_status': nzb['status'], 'original_status': nzb['status'],
'timeleft': nzb['timeleft'] if not queue['paused'] else -1, 'timeleft': nzb['timeleft'] if not queue['paused'] else -1,
}) })
# Get old releases # Get old releases
for nzb in history.get('slots', []): for nzb in history.get('slots', []):
if nzb['nzo_id'] in ids:
status = 'busy'
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()):
status = 'failed'
elif nzb['status'] == 'Completed':
status = 'completed'
status = 'busy' release_downloads.append({
if nzb['status'] == 'Failed' or (nzb['status'] == 'Completed' and nzb['fail_message'].strip()): 'id': nzb['nzo_id'],
status = 'failed' 'name': nzb['name'],
elif nzb['status'] == 'Completed': 'status': status,
status = 'completed' 'original_status': nzb['status'],
'timeleft': str(timedelta(seconds = 0)),
release_downloads.append({ 'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
'id': nzb['nzo_id'], })
'name': nzb['name'],
'status': status,
'original_status': nzb['status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': sp(os.path.dirname(nzb['storage']) if os.path.isfile(nzb['storage']) else nzb['storage']),
})
return release_downloads return release_downloads
@@ -164,9 +185,9 @@ class Sabnzbd(Downloader):
def call(self, request_params, use_json = True, **kwargs): def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, { url = cleanHost(self.conf('host'), ssl = self.conf('ssl')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'output': 'json' 'output': 'json'
})) }))
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs) data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
@@ -1,5 +1,6 @@
from .main import Synology from .main import Synology
def start(): def start():
return Synology() return Synology()
+23 -8
View File
@@ -1,5 +1,6 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import json import json
import requests import requests
@@ -11,17 +12,17 @@ log = CPLog(__name__)
class Synology(Downloader): class Synology(Downloader):
protocol = ['nzb', 'torrent', 'torrent_magnet'] protocol = ['nzb', 'torrent', 'torrent_magnet']
log = CPLog(__name__) status_support = False
def download(self, data = None, movie = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
response = False response = False
log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol'])) log.error('Sending "%s" (%s) to Synology.', (data['name'], data['protocol']))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
@@ -42,7 +43,17 @@ class Synology(Downloader):
except: except:
log.error('Exception while adding torrent: %s', traceback.format_exc()) log.error('Exception while adding torrent: %s', traceback.format_exc())
finally: finally:
return response return self.downloadReturnId('') if response else False
def test(self):
host = cleanHost(self.conf('host'), protocol = False).split(':')
try:
srpc = SynologyRPC(host[0], host[1], self.conf('username'), self.conf('password'))
test_result = srpc.test()
except:
return False
return test_result
def getEnabledProtocol(self): def getEnabledProtocol(self):
if self.conf('use_for') == 'both': if self.conf('use_for') == 'both':
@@ -64,6 +75,7 @@ class Synology(Downloader):
return super(Synology, self).isEnabled(manual, data) and\ return super(Synology, self).isEnabled(manual, data) and\
((self.conf('use_for') in for_protocol)) ((self.conf('use_for') in for_protocol))
class SynologyRPC(object): class SynologyRPC(object):
"""SynologyRPC lite library""" """SynologyRPC lite library"""
@@ -106,11 +118,11 @@ class SynologyRPC(object):
if response['success']: if response['success']:
log.info('Synology action successfull') log.info('Synology action successfull')
return response return response
except requests.ConnectionError, err: except requests.ConnectionError as err:
log.error('Synology connection error, check your config %s', err) log.error('Synology connection error, check your config %s', err)
except requests.HTTPError, err: except requests.HTTPError as err:
log.error('SynologyRPC HTTPError: %s', err) log.error('SynologyRPC HTTPError: %s', err)
except Exception, err: except Exception as err:
log.error('Exception: %s', err) log.error('Exception: %s', err)
finally: finally:
return response return response
@@ -145,3 +157,6 @@ class SynologyRPC(object):
self._logout() self._logout()
return result return result
def test(self):
return bool(self._login())
@@ -1,5 +1,6 @@
from .main import Transmission from .main import Transmission
def start(): def start():
return Transmission() return Transmission()
@@ -1,7 +1,7 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, sp from couchpotato.core.helpers.encoding import isInt, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
import httplib import httplib
@@ -19,19 +19,21 @@ class Transmission(Downloader):
log = CPLog(__name__) log = CPLog(__name__)
trpc = None trpc = None
def connect(self): def connect(self, reconnect = False):
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
if not self.trpc: if not self.trpc or reconnect:
self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url'), username = self.conf('username'), password = self.conf('password')) self.trpc = TransmissionRPC(host[0], port = host[1], rpc_url = self.conf('rpc_url').strip('/ '), username = self.conf('username'), password = self.conf('password'))
return self.trpc return self.trpc
def download(self, data, movie, filedata = None): def download(self, data = None, media = None, filedata = None):
if not media: media = {}
if not data: data = {}
log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol'))) log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('protocol')))
@@ -81,12 +83,17 @@ class Transmission(Downloader):
log.info('Torrent sent to Transmission successfully.') log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
def getAllDownloadStatus(self): def test(self):
if self.connect(True) and self.trpc.get_session():
return True
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking Transmission download status.') log.debug('Checking Transmission download status.')
if not self.connect(): if not self.connect():
return False return []
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
@@ -94,37 +101,44 @@ class Transmission(Downloader):
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files'] 'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isStalled', 'isFinished', 'downloadDir', 'uploadRatio', 'secondsSeeding', 'seedIdleLimit', 'files']
} }
session = self.trpc.get_session()
queue = self.trpc.get_alltorrents(return_params) queue = self.trpc.get_alltorrents(return_params)
if not (queue and queue.get('torrents')): if not (queue and queue.get('torrents')):
log.debug('Nothing in queue or error') log.debug('Nothing in queue or error')
return False return []
for torrent in queue['torrents']: for torrent in queue['torrents']:
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / isFinished=%s', if torrent['hashString'] in ids:
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent['eta'], torrent['uploadRatio'], torrent['isFinished'])) log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / isStalled=%s / eta=%s / uploadRatio=%s / isFinished=%s / incomplete-dir-enabled=%s / incomplete-dir=%s',
(torrent['name'], torrent['id'], torrent['downloadDir'], torrent['hashString'], torrent['percentDone'], torrent['status'], torrent.get('isStalled', 'N/A'), torrent['eta'], torrent['uploadRatio'], torrent['isFinished'], session['incomplete-dir-enabled'], session['incomplete-dir']))
torrent_files = [] status = 'busy'
for file_item in torrent['files']: if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
torrent_files.append(sp(os.path.join(torrent['downloadDir'], file_item['name']))) status = 'failed'
elif torrent['status'] == 0 and torrent['percentDone'] == 1:
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
status = 'busy' if session['incomplete-dir-enabled'] and status == 'busy':
if torrent.get('isStalled') and self.conf('stalled_as_failed'): torrent_folder = session['incomplete-dir']
status = 'failed' else:
elif torrent['status'] == 0 and torrent['percentDone'] == 1: torrent_folder = torrent['downloadDir']
status = 'completed'
elif torrent['status'] in [5, 6]:
status = 'seeding'
release_downloads.append({ torrent_files = []
'id': torrent['hashString'], for file_item in torrent['files']:
'name': torrent['name'], torrent_files.append(sp(os.path.join(torrent_folder, file_item['name'])))
'status': status,
'original_status': torrent['status'], release_downloads.append({
'seed_ratio': torrent['uploadRatio'], 'id': torrent['hashString'],
'timeleft': str(timedelta(seconds = torrent['eta'])), 'name': torrent['name'],
'folder': sp(torrent['downloadDir'] if len(torrent_files) == 1 else os.path.join(torrent['downloadDir'], torrent['name'])), 'status': status,
'files': '|'.join(torrent_files) 'original_status': torrent['status'],
}) 'seed_ratio': torrent['uploadRatio'],
'timeleft': str(timedelta(seconds = torrent['eta'])),
'folder': sp(torrent_folder if len(torrent_files) == 1 else os.path.join(torrent_folder, torrent['name'])),
'files': '|'.join(torrent_files)
})
return release_downloads return release_downloads
@@ -178,10 +192,10 @@ class TransmissionRPC(object):
else: else:
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result']) log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
return False return False
except httplib.InvalidURL, err: except httplib.InvalidURL as err:
log.error('Invalid Transmission host, check your config %s', err) log.error('Invalid Transmission host, check your config %s', err)
return False return False
except urllib2.HTTPError, err: except urllib2.HTTPError as err:
if err.code == 401: if err.code == 401:
log.error('Invalid Transmission Username or Password, check your config') log.error('Invalid Transmission Username or Password, check your config')
return False return False
@@ -199,7 +213,7 @@ class TransmissionRPC(object):
log.error('Unable to get Transmission Session-Id %s', err) log.error('Unable to get Transmission Session-Id %s', err)
else: else:
log.error('TransmissionRPC HTTPError: %s', err) log.error('TransmissionRPC HTTPError: %s', err)
except urllib2.URLError, err: except urllib2.URLError as err:
log.error('Unable to connect to Transmission %s', err) log.error('Unable to connect to Transmission %s', err)
def get_session(self): def get_session(self):
@@ -1,5 +1,6 @@
from .main import uTorrent from .main import uTorrent
def start(): def start():
return uTorrent() return uTorrent()
@@ -23,7 +24,7 @@ config = [{
{ {
'name': 'host', 'name': 'host',
'default': 'localhost:8000', 'default': 'localhost:8000',
'description': 'Hostname with port. Usually <strong>localhost:8000</strong>', 'description': 'Port can be found in settings when enabling WebUI.',
}, },
{ {
'name': 'username', 'name': 'username',
+92 -73
View File
@@ -2,7 +2,7 @@ from base64 import b16encode, b32decode
from bencode import bencode as benc, bdecode from bencode import bencode as benc, bdecode
from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList from couchpotato.core.downloaders.base import Downloader, ReleaseDownloadList
from couchpotato.core.helpers.encoding import isInt, ss, sp from couchpotato.core.helpers.encoding import isInt, ss, sp
from couchpotato.core.helpers.variable import tryInt, tryFloat from couchpotato.core.helpers.variable import tryInt, tryFloat, cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta from datetime import timedelta
from hashlib import sha1 from hashlib import sha1
@@ -24,10 +24,20 @@ class uTorrent(Downloader):
protocol = ['torrent', 'torrent_magnet'] protocol = ['torrent', 'torrent_magnet']
utorrent_api = None utorrent_api = None
status_flags = {
'STARTED' : 1,
'CHECKING' : 2,
'CHECK-START' : 4,
'CHECKED' : 8,
'ERROR' : 16,
'PAUSED' : 32,
'QUEUED' : 64,
'LOADED' : 128
}
def connect(self): def connect(self):
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = cleanHost(self.conf('host'), protocol = False).split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error('Config properties are not filled in correctly, port is missing.') log.error('Config properties are not filled in correctly, port is missing.')
return False return False
@@ -36,11 +46,11 @@ class uTorrent(Downloader):
return self.utorrent_api return self.utorrent_api
def download(self, data = None, movie = None, filedata = None): def download(self, data = None, media = None, filedata = None):
if not movie: movie = {} if not media: media = {}
if not data: data = {} if not data: data = {}
log.debug('Sending "%s" (%s) to uTorrent.', (data.get('name'), data.get('protocol'))) log.debug("Sending '%s' (%s) to uTorrent.", (data.get('name'), data.get('protocol')))
if not self.connect(): if not self.connect():
return False return False
@@ -56,7 +66,7 @@ class uTorrent(Downloader):
new_settings['seed_prio_limitul_flag'] = True new_settings['seed_prio_limitul_flag'] = True
log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.') log.info('Updated uTorrent settings to set a torrent to complete after it the seeding requirements are met.')
if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function if settings.get('bt.read_only_on_complete'): #This doesn't work as this option seems to be not available through the api. Mitigated with removeReadOnly function
new_settings['bt.read_only_on_complete'] = False new_settings['bt.read_only_on_complete'] = False
log.info('Updated uTorrent settings to not set the files to read only after completing.') log.info('Updated uTorrent settings to not set the files to read only after completing.')
@@ -75,9 +85,10 @@ class uTorrent(Downloader):
torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper() torrent_hash = re.findall('urn:btih:([\w]{32,40})', data.get('url'))[0].upper()
torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers) torrent_params['trackers'] = '%0D%0A%0D%0A'.join(self.torrent_trackers)
else: else:
info = bdecode(filedata)["info"] info = bdecode(filedata)['info']
torrent_hash = sha1(benc(info)).hexdigest().upper() torrent_hash = sha1(benc(info)).hexdigest().upper()
torrent_filename = self.createFileName(data, filedata, movie)
torrent_filename = self.createFileName(data, filedata, media)
if data.get('seed_ratio'): if data.get('seed_ratio'):
torrent_params['seed_override'] = 1 torrent_params['seed_override'] = 1
@@ -104,72 +115,73 @@ class uTorrent(Downloader):
return self.downloadReturnId(torrent_hash) return self.downloadReturnId(torrent_hash)
def getAllDownloadStatus(self): def test(self):
if self.connect():
build_version = self.utorrent_api.get_build()
if not build_version:
return False
if build_version < 25406: # This build corresponds to version 3.0.0 stable
return False, 'Your uTorrent client is too old, please update to newest version.'
return True
return False
def getAllDownloadStatus(self, ids):
log.debug('Checking uTorrent download status.') log.debug('Checking uTorrent download status.')
if not self.connect(): if not self.connect():
return False return []
release_downloads = ReleaseDownloadList(self) release_downloads = ReleaseDownloadList(self)
data = self.utorrent_api.get_status() data = self.utorrent_api.get_status()
if not data: if not data:
log.error('Error getting data from uTorrent') log.error('Error getting data from uTorrent')
return False return []
queue = json.loads(data) queue = json.loads(data)
if queue.get('error'): if queue.get('error'):
log.error('Error getting data from uTorrent: %s', queue.get('error')) log.error('Error getting data from uTorrent: %s', queue.get('error'))
return False return []
if not queue.get('torrents'): if not queue.get('torrents'):
log.debug('Nothing in queue') log.debug('Nothing in queue')
return False return []
# Get torrents # Get torrents
for torrent in queue['torrents']: for torrent in queue['torrents']:
if torrent[0] in ids:
#Get files of the torrent #Get files of the torrent
torrent_files = [] torrent_files = []
try: try:
torrent_files = json.loads(self.utorrent_api.get_files(torrent[0])) torrent_files = json.loads(self.utorrent_api.get_files(torrent[0]))
torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]] torrent_files = [sp(os.path.join(torrent[26], torrent_file[0])) for torrent_file in torrent_files['files'][1]]
except: except:
log.debug('Failed getting files from torrent: %s', torrent[2]) log.debug('Failed getting files from torrent: %s', torrent[2])
status_flags = { status = 'busy'
"STARTED" : 1, if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
"CHECKING" : 2, status = 'seeding'
"CHECK-START" : 4, elif (torrent[1] & self.status_flags['ERROR']):
"CHECKED" : 8, status = 'failed'
"ERROR" : 16, elif torrent[4] == 1000:
"PAUSED" : 32, status = 'completed'
"QUEUED" : 64,
"LOADED" : 128
}
status = 'busy' if not status == 'busy':
if (torrent[1] & status_flags["STARTED"] or torrent[1] & status_flags["QUEUED"]) and torrent[4] == 1000: self.removeReadOnly(torrent_files)
status = 'seeding'
elif (torrent[1] & status_flags["ERROR"]):
status = 'failed'
elif torrent[4] == 1000:
status = 'completed'
if not status == 'busy': release_downloads.append({
self.removeReadOnly(torrent_files) 'id': torrent[0],
'name': torrent[2],
release_downloads.append({ 'status': status,
'id': torrent[0], 'seed_ratio': float(torrent[7]) / 1000,
'name': torrent[2], 'original_status': torrent[1],
'status': status, 'timeleft': str(timedelta(seconds = torrent[10])),
'seed_ratio': float(torrent[7]) / 1000, 'folder': sp(torrent[26]),
'original_status': torrent[1], 'files': '|'.join(torrent_files)
'timeleft': str(timedelta(seconds = torrent[10])), })
'folder': sp(torrent[26]),
'files': '|'.join(torrent_files)
})
return release_downloads return release_downloads
@@ -222,7 +234,7 @@ class uTorrentAPI(object):
if time.time() > self.last_time + 1800: if time.time() > self.last_time + 1800:
self.last_time = time.time() self.last_time = time.time()
self.token = self.get_token() self.token = self.get_token()
request = urllib2.Request(self.url + "?token=" + self.token + "&" + action, data) request = urllib2.Request(self.url + '?token=' + self.token + '&' + action, data)
try: try:
open_request = self.opener.open(request) open_request = self.opener.open(request)
response = open_request.read() response = open_request.read()
@@ -230,64 +242,64 @@ class uTorrentAPI(object):
return response return response
else: else:
log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response) log.debug('Unknown failure sending command to uTorrent. Return text is: %s', response)
except httplib.InvalidURL, err: except httplib.InvalidURL as err:
log.error('Invalid uTorrent host, check your config %s', err) log.error('Invalid uTorrent host, check your config %s', err)
except urllib2.HTTPError, err: except urllib2.HTTPError as err:
if err.code == 401: if err.code == 401:
log.error('Invalid uTorrent Username or Password, check your config') log.error('Invalid uTorrent Username or Password, check your config')
else: else:
log.error('uTorrent HTTPError: %s', err) log.error('uTorrent HTTPError: %s', err)
except urllib2.URLError, err: except urllib2.URLError as err:
log.error('Unable to connect to uTorrent %s', err) log.error('Unable to connect to uTorrent %s', err)
return False return False
def get_token(self): def get_token(self):
request = self.opener.open(self.url + "token.html") request = self.opener.open(self.url + 'token.html')
token = re.findall("<div.*?>(.*?)</", request.read())[0] token = re.findall('<div.*?>(.*?)</', request.read())[0]
return token return token
def add_torrent_uri(self, filename, torrent, add_folder = False): def add_torrent_uri(self, filename, torrent, add_folder = False):
action = "action=add-url&s=%s" % urllib.quote(torrent) action = 'action=add-url&s=%s' % urllib.quote(torrent)
if add_folder: if add_folder:
action += "&path=%s" % urllib.quote(filename) action += '&path=%s' % urllib.quote(filename)
return self._request(action) return self._request(action)
def add_torrent_file(self, filename, filedata, add_folder = False): def add_torrent_file(self, filename, filedata, add_folder = False):
action = "action=add-file" action = 'action=add-file'
if add_folder: if add_folder:
action += "&path=%s" % urllib.quote(filename) action += '&path=%s' % urllib.quote(filename)
return self._request(action, {"torrent_file": (ss(filename), filedata)}) return self._request(action, {'torrent_file': (ss(filename), filedata)})
def set_torrent(self, hash, params): def set_torrent(self, hash, params):
action = "action=setprops&hash=%s" % hash action = 'action=setprops&hash=%s' % hash
for k, v in params.iteritems(): for k, v in params.items():
action += "&s=%s&v=%s" % (k, v) action += '&s=%s&v=%s' % (k, v)
return self._request(action) return self._request(action)
def pause_torrent(self, hash, pause = True): def pause_torrent(self, hash, pause = True):
if pause: if pause:
action = "action=pause&hash=%s" % hash action = 'action=pause&hash=%s' % hash
else: else:
action = "action=unpause&hash=%s" % hash action = 'action=unpause&hash=%s' % hash
return self._request(action) return self._request(action)
def stop_torrent(self, hash): def stop_torrent(self, hash):
action = "action=stop&hash=%s" % hash action = 'action=stop&hash=%s' % hash
return self._request(action) return self._request(action)
def remove_torrent(self, hash, remove_data = False): def remove_torrent(self, hash, remove_data = False):
if remove_data: if remove_data:
action = "action=removedata&hash=%s" % hash action = 'action=removedata&hash=%s' % hash
else: else:
action = "action=remove&hash=%s" % hash action = 'action=remove&hash=%s' % hash
return self._request(action) return self._request(action)
def get_status(self): def get_status(self):
action = "list=1" action = 'list=1'
return self._request(action) return self._request(action)
def get_settings(self): def get_settings(self):
action = "action=getsettings" action = 'action=getsettings'
settings_dict = {} settings_dict = {}
try: try:
utorrent_settings = json.loads(self._request(action)) utorrent_settings = json.loads(self._request(action))
@@ -303,7 +315,7 @@ class uTorrentAPI(object):
#log.debug('uTorrent settings: %s', settings_dict) #log.debug('uTorrent settings: %s', settings_dict)
except Exception, err: except Exception as err:
log.error('Failed to get settings from uTorrent: %s', err) log.error('Failed to get settings from uTorrent: %s', err)
return settings_dict return settings_dict
@@ -319,5 +331,12 @@ class uTorrentAPI(object):
return self._request(action) return self._request(action)
def get_files(self, hash): def get_files(self, hash):
action = "action=getfiles&hash=%s" % hash action = 'action=getfiles&hash=%s' % hash
return self._request(action) return self._request(action)
def get_build(self):
data = self._request('')
if not data:
return False
response = json.loads(data)
return int(response.get('build'))
+19 -14
View File
@@ -1,5 +1,5 @@
from axl.axel import Event from axl.axel import Event
from couchpotato.core.helpers.variable import mergeDicts, natcmp from couchpotato.core.helpers.variable import mergeDicts, natsortKey
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import threading import threading
import traceback import traceback
@@ -7,6 +7,7 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
events = {} events = {}
def runHandler(name, handler, *args, **kwargs): def runHandler(name, handler, *args, **kwargs):
try: try:
return handler(*args, **kwargs) return handler(*args, **kwargs)
@@ -14,6 +15,7 @@ def runHandler(name, handler, *args, **kwargs):
from couchpotato.environment import Env from couchpotato.environment import Env
log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else '')) log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all() if not Env.get('dev') else ''))
def addEvent(name, handler, priority = 100): def addEvent(name, handler, priority = 100):
if not events.get(name): if not events.get(name):
@@ -27,7 +29,7 @@ def addEvent(name, handler, priority = 100):
has_parent = hasattr(handler, 'im_self') has_parent = hasattr(handler, 'im_self')
parent = None parent = None
if has_parent: if has_parent:
parent = handler.im_self parent = handler.__self__
bc = hasattr(parent, 'beforeCall') bc = hasattr(parent, 'beforeCall')
if bc: parent.beforeCall(handler) if bc: parent.beforeCall(handler)
@@ -48,22 +50,19 @@ def addEvent(name, handler, priority = 100):
'priority': priority, 'priority': priority,
}) })
def removeEvent(name, handler):
e = events[name]
e -= handler
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.has_key(name): return if name not in events: return
#log.debug('Firing event %s', name) #log.debug('Firing event %s', name)
try: try:
options = { options = {
'is_after_event': False, # Fire after event 'is_after_event': False, # Fire after event
'on_complete': False, # onComplete event 'on_complete': False, # onComplete event
'single': False, # Return single handler 'single': False, # Return single handler
'merge': False, # Merge items 'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish 'in_order': False, # Fire them in specific order, waits for the other to finish
} }
# Do options # Do options
@@ -101,11 +100,14 @@ def fireEvent(name, *args, **kwargs):
# Fire # Fire
result = e(*args, **kwargs) result = e(*args, **kwargs)
result_keys = result.keys()
result_keys.sort(key = natsortKey)
if options['single'] and not options['merge']: if options['single'] and not options['merge']:
results = None results = None
# Loop over results, stop when first not None result is found. # Loop over results, stop when first not None result is found.
for r_key in sorted(result.iterkeys(), cmp = natcmp): for r_key in result_keys:
r = result[r_key] r = result[r_key]
if r[0] is True and r[1] is not None: if r[0] is True and r[1] is not None:
results = r[1] results = r[1]
@@ -117,7 +119,7 @@ def fireEvent(name, *args, **kwargs):
else: else:
results = [] results = []
for r_key in sorted(result.iterkeys(), cmp = natcmp): for r_key in result_keys:
r = result[r_key] r = result[r_key]
if r[0] == True and r[1]: if r[0] == True and r[1]:
results.append(r[1]) results.append(r[1])
@@ -160,18 +162,21 @@ def fireEvent(name, *args, **kwargs):
except Exception: except Exception:
log.error('%s: %s', (name, traceback.format_exc())) log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs): def fireEventAsync(*args, **kwargs):
try: try:
t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs) t = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
t.setDaemon(True) t.setDaemon(True)
t.start() t.start()
return True return True
except Exception, e: except Exception as e:
log.error('%s: %s', (args[0], e)) log.error('%s: %s', (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error
log.error(''.join(traceback.format_exception(etype, value, tb))) log.error(''.join(traceback.format_exception(etype, value, tb)))
def getEvent(name): def getEvent(name):
return events[name] return events[name]
+38 -8
View File
@@ -5,29 +5,32 @@ import os
import re import re
import traceback import traceback
import unicodedata import unicodedata
import six
log = CPLog(__name__) log = CPLog(__name__)
def toSafeString(original): def toSafeString(original):
valid_chars = "-_.() %s%s" % (ascii_letters, digits) valid_chars = "-_.() %s%s" % (ascii_letters, digits)
cleanedFilename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore') cleaned_filename = unicodedata.normalize('NFKD', toUnicode(original)).encode('ASCII', 'ignore')
valid_string = ''.join(c for c in cleanedFilename if c in valid_chars) valid_string = ''.join(c for c in cleaned_filename if c in valid_chars)
return ' '.join(valid_string.split()) return ' '.join(valid_string.split())
def simplifyString(original): def simplifyString(original):
string = stripAccents(original.lower()) string = stripAccents(original.lower())
string = toSafeString(' '.join(re.split('\W+', string))) string = toSafeString(' '.join(re.split('\W+', string)))
split = re.split('\W+|_', string.lower()) split = re.split('\W+|_', string.lower())
return toUnicode(' '.join(split)) return toUnicode(' '.join(split))
def toUnicode(original, *args): def toUnicode(original, *args):
try: try:
if isinstance(original, unicode): if isinstance(original, unicode):
return original return original
else: else:
try: try:
return unicode(original, *args) return six.text_type(original, *args)
except: except:
try: try:
return ek(original, *args) return ek(original, *args)
@@ -38,19 +41,43 @@ def toUnicode(original, *args):
ascii_text = str(original).encode('string_escape') ascii_text = str(original).encode('string_escape')
return toUnicode(ascii_text) return toUnicode(ascii_text)
def ss(original, *args): def ss(original, *args):
u_original = toUnicode(original, *args) u_original = toUnicode(original, *args)
try: try:
from couchpotato.environment import Env from couchpotato.environment import Env
return u_original.encode(Env.get('encoding')) return u_original.encode(Env.get('encoding'))
except Exception, e: except Exception as e:
log.debug('Failed ss encoding char, force UTF8: %s', e) log.debug('Failed ss encoding char, force UTF8: %s', e)
return u_original.encode('UTF-8') return u_original.encode('UTF-8')
def sp(path, *args): def sp(path, *args):
# Standardise encoding, normalise case, path and strip trailing '/' or '\' # Standardise encoding, normalise case, path and strip trailing '/' or '\'
return os.path.normcase(os.path.normpath(ss(path, *args))).rstrip(os.path.sep) if not path or len(path) == 0:
return path
# convert windows path (from remote box) to *nix path
if os.path.sep == '/' and '\\' in path:
path = '/' + path.replace(':', '').replace('\\', '/')
path = os.path.normpath(ss(path, *args))
# Remove any trailing path separators
if path != os.path.sep:
path = path.rstrip(os.path.sep)
# Add a trailing separator in case it is a root folder on windows (crashes guessit)
if len(path) == 2 and path[1] == ':':
path = path + os.path.sep
# Replace *NIX ambiguous '//' at the beginning of a path with '/' (crashes guessit)
path = re.sub('^//', '/', path)
return path
def ek(original, *args): def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
@@ -62,6 +89,7 @@ def ek(original, *args):
return original return original
def isInt(value): def isInt(value):
try: try:
int(value) int(value)
@@ -69,14 +97,16 @@ def isInt(value):
except ValueError: except ValueError:
return False return False
def stripAccents(s): def stripAccents(s):
return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn')) return ''.join((c for c in unicodedata.normalize('NFD', toUnicode(s)) if unicodedata.category(c) != 'Mn'))
def tryUrlencode(s): def tryUrlencode(s):
new = u'' new = six.u('')
if isinstance(s, dict): if isinstance(s, dict):
for key, value in s.iteritems(): for key, value in s.items():
new += u'&%s=%s' % (key, tryUrlencode(value)) new += six.u('&%s=%s') % (key, tryUrlencode(value))
return new[1:] return new[1:]
else: else:
+13 -4
View File
@@ -1,5 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp from couchpotato.core.helpers.variable import natsortKey
from urllib import unquote from urllib import unquote
import re import re
@@ -8,8 +8,13 @@ def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$') reg = re.compile('^[a-z0-9_\.]+$')
# Sort keys
param_keys = params.keys()
param_keys.sort(key = natsortKey)
temp = {} temp = {}
for param, value in sorted(params.iteritems()): for param in param_keys:
value = params[param]
nest = re.split("([\[\]]+)", param) nest = re.split("([\[\]]+)", param)
if len(nest) > 1: if len(nest) > 1:
@@ -37,13 +42,17 @@ def getParams(params):
return dictToList(temp) return dictToList(temp)
def dictToList(params): def dictToList(params):
if type(params) is dict: if type(params) is dict:
new = {} new = {}
for x, value in params.iteritems(): for x, value in params.items():
try: try:
new_value = [dictToList(value[k]) for k in sorted(value.iterkeys(), cmp = natcmp)] convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key)]
sorted_keys = sorted(value.keys(), key = alphanum_key)
new_value = [dictToList(value[k]) for k in sorted_keys]
except: except:
new_value = value new_value = value
+2 -1
View File
@@ -3,6 +3,7 @@ import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
class RSS(object): class RSS(object):
def getTextElements(self, xml, path): def getTextElements(self, xml, path):
@@ -46,6 +47,6 @@ class RSS(object):
def getItems(self, data, path = 'channel/item'): def getItems(self, data, path = 'channel/item'):
try: try:
return XMLTree.parse(data).findall(path) return XMLTree.parse(data).findall(path)
except Exception, e: except Exception as e:
log.error('Error parsing RSS. %s', e) log.error('Error parsing RSS. %s', e)
return [] return []
+101 -18
View File
@@ -2,29 +2,38 @@ from couchpotato.core.helpers.encoding import simplifyString, toSafeString, ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
import collections import collections
import hashlib import hashlib
import os.path import os
import platform import platform
import random import random
import re import re
import string import string
import sys import sys
import six
from six.moves import map, zip, filter
log = CPLog(__name__) log = CPLog(__name__)
def fnEscape(pattern):
return pattern.replace('[', '[[').replace(']', '[]]').replace('[[', '[[]')
def link(src, dst): def link(src, dst):
if os.name == 'nt': if os.name == 'nt':
import ctypes import ctypes
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError() if ctypes.windll.kernel32.CreateHardLinkW(six.text_type(dst), six.text_type(src), 0) == 0: raise ctypes.WinError()
else: else:
os.link(src, dst) os.link(src, dst)
def symlink(src, dst): def symlink(src, dst):
if os.name == 'nt': if os.name == 'nt':
import ctypes import ctypes
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError() if ctypes.windll.kernel32.CreateSymbolicLinkW(six.text_type(dst), six.text_type(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
else: else:
os.symlink(src, dst) os.symlink(src, dst)
def getUserDir(): def getUserDir():
try: try:
import pwd import pwd
@@ -34,6 +43,7 @@ def getUserDir():
return os.path.expanduser('~') return os.path.expanduser('~')
def getDownloadDir(): def getDownloadDir():
user_dir = getUserDir() user_dir = getUserDir()
@@ -46,6 +56,7 @@ def getDownloadDir():
return user_dir return user_dir
def getDataDir(): def getDataDir():
# Windows # Windows
@@ -65,8 +76,10 @@ def getDataDir():
# Linux # Linux
return os.path.join(user_dir, '.couchpotato') return os.path.join(user_dir, '.couchpotato')
def isDict(object):
return isinstance(object, dict) def isDict(obj):
return isinstance(obj, dict)
def mergeDicts(a, b, prepend_list = False): def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b) assert isDict(a), isDict(b)
@@ -88,6 +101,7 @@ def mergeDicts(a, b, prepend_list = False):
current_dst[key] = current_src[key] current_dst[key] = current_src[key]
return dst return dst
def removeListDuplicates(seq): def removeListDuplicates(seq):
checked = [] checked = []
for e in seq: for e in seq:
@@ -95,35 +109,73 @@ def removeListDuplicates(seq):
checked.append(e) checked.append(e)
return checked return checked
def flattenList(l): def flattenList(l):
if isinstance(l, list): if isinstance(l, list):
return sum(map(flattenList, l)) return sum(map(flattenList, l))
else: else:
return l return l
def md5(text): def md5(text):
return hashlib.md5(ss(text)).hexdigest() return hashlib.md5(ss(text)).hexdigest()
def sha1(text): def sha1(text):
return hashlib.sha1(text).hexdigest() return hashlib.sha1(text).hexdigest()
def isLocalIP(ip): def isLocalIP(ip):
ip = ip.lstrip('htps:/') ip = ip.lstrip('htps:/')
regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/' regex = '/(^127\.)|(^192\.168\.)|(^10\.)|(^172\.1[6-9]\.)|(^172\.2[0-9]\.)|(^172\.3[0-1]\.)|(^::1)$/'
return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.' return re.search(regex, ip) is not None or 'localhost' in ip or ip[:4] == '127.'
def getExt(filename): def getExt(filename):
return os.path.splitext(filename)[1][1:] return os.path.splitext(filename)[1][1:]
def cleanHost(host):
if not host.startswith(('http://', 'https://')):
host = 'http://' + host
host = host.rstrip('/') def cleanHost(host, protocol = True, ssl = False, username = None, password = None):
host += '/' """Return a cleaned up host with given url options set
Changes protocol to https if ssl is set to True and http if ssl is set to false.
>>> cleanHost("localhost:80", ssl=True)
'https://localhost:80/'
>>> cleanHost("localhost:80", ssl=False)
'http://localhost:80/'
Username and password is managed with the username and password variables
>>> cleanHost("localhost:80", username="user", password="passwd")
'http://user:passwd@localhost:80/'
Output without scheme (protocol) can be forced with protocol=False
>>> cleanHost("localhost:80", protocol=False)
'localhost:80'
"""
if not '://' in host and protocol:
host = ('https://' if ssl else 'http://') + host
if not protocol:
host = host.split('://', 1)[-1]
if protocol and username and password:
try:
auth = re.findall('^(?:.+?//)(.+?):(.+?)@(?:.+)$', host)
if auth:
log.error('Cleanhost error: auth already defined in url: %s, please remove BasicAuth from url.', host)
else:
host = host.replace('://', '://%s:%s@' % (username, password), 1)
except:
pass
host = host.rstrip('/ ')
if protocol:
host += '/'
return host return host
def getImdb(txt, check_inside = False, multiple = False): def getImdb(txt, check_inside = False, multiple = False):
if not check_inside: if not check_inside:
@@ -140,7 +192,7 @@ def getImdb(txt, check_inside = False, multiple = False):
ids = re.findall('(tt\d{4,7})', txt) ids = re.findall('(tt\d{4,7})', txt)
if multiple: if multiple:
return list(set(['tt%07d' % tryInt(x[2:]) for x in ids])) if len(ids) > 0 else [] return removeDuplicate(['tt%07d' % tryInt(x[2:]) for x in ids]) if len(ids) > 0 else []
return 'tt%07d' % tryInt(ids[0][2:]) return 'tt%07d' % tryInt(ids[0][2:])
except IndexError: except IndexError:
@@ -148,10 +200,12 @@ def getImdb(txt, check_inside = False, multiple = False):
return False return False
def tryInt(s, default = 0): def tryInt(s, default = 0):
try: return int(s) try: return int(s)
except: return default except: return default
def tryFloat(s): def tryFloat(s):
try: try:
if isinstance(s, str): if isinstance(s, str):
@@ -160,17 +214,17 @@ def tryFloat(s):
return float(s) return float(s)
except: return 0 except: return 0
def natsortKey(s): def natsortKey(string_):
return map(tryInt, re.findall(r'(\d+|\D+)', s)) """See http://www.codinghorror.com/blog/archives/001018.html"""
return [int(s) if s.isdigit() else s for s in re.split(r'(\d+)', string_)]
def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b))
def toIterable(value): def toIterable(value):
if isinstance(value, collections.Iterable): if isinstance(value, collections.Iterable):
return value return value
return [value] return [value]
def getTitle(library_dict): def getTitle(library_dict):
try: try:
try: try:
@@ -193,6 +247,7 @@ def getTitle(library_dict):
log.error('Could not get title for library item: %s', library_dict) log.error('Could not get title for library item: %s', library_dict)
return None return None
def possibleTitles(raw_title): def possibleTitles(raw_title):
titles = [ titles = [
@@ -205,14 +260,42 @@ def possibleTitles(raw_title):
new_title = raw_title.replace('&', 'and') new_title = raw_title.replace('&', 'and')
titles.append(simplifyString(new_title)) titles.append(simplifyString(new_title))
return list(set(titles)) return removeDuplicate(titles)
def randomString(size = 8, chars = string.ascii_uppercase + string.digits): def randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size)) return ''.join(random.choice(chars) for x in range(size))
def splitString(str, split_on = ',', clean = True): def splitString(str, split_on = ',', clean = True):
list = [x.strip() for x in str.split(split_on)] if str else [] l = [x.strip() for x in str.split(split_on)] if str else []
return filter(None, list) if clean else list return removeEmpty(l) if clean else l
def removeEmpty(l):
return list(filter(None, l))
def removeDuplicate(l):
seen = set()
return [x for x in l if x not in seen and not seen.add(x)]
def dictIsSubset(a, b): def dictIsSubset(a, b):
return all([k in b and b[k] == v for k, v in a.items()]) return all([k in b and b[k] == v for k, v in a.items()])
def isSubFolder(sub_folder, base_folder):
# Returns True if sub_folder is the same as or inside base_folder
return base_folder and sub_folder and ss(os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep) in ss(os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep)
# From SABNZBD
re_password = [re.compile(r'([^/\\]+)[/\\](.+)'), re.compile(r'(.+){{([^{}]+)}}$'), re.compile(r'(.+)\s+password\s*=\s*(.+)$', re.I)]
def scanForPassword(name):
m = None
for reg in re_password:
m = reg.search(name)
if m: break
if m:
return m.group(1).strip('. '), m.group(2).strip()
+5 -4
View File
@@ -1,9 +1,10 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from importlib import import_module from importhelper import import_module
import os import os
import sys import sys
import traceback import traceback
import six
log = CPLog(__name__) log = CPLog(__name__)
@@ -37,7 +38,7 @@ class Loader(object):
self.paths['custom_plugins'] = (30, '', custom_plugin_dir) self.paths['custom_plugins'] = (30, '', custom_plugin_dir)
# Loop over all paths and add to module list # Loop over all paths and add to module list
for plugin_type, plugin_tuple in self.paths.iteritems(): for plugin_type, plugin_tuple in self.paths.items():
priority, module, dir_name = plugin_tuple priority, module, dir_name = plugin_tuple
self.addFromDir(plugin_type, priority, module, dir_name) self.addFromDir(plugin_type, priority, module, dir_name)
@@ -45,7 +46,7 @@ class Loader(object):
did_save = 0 did_save = 0
for priority in sorted(self.modules): for priority in sorted(self.modules):
for module_name, plugin in sorted(self.modules[priority].iteritems()): for module_name, plugin in sorted(self.modules[priority].items()):
# Load module # Load module
try: try:
@@ -81,7 +82,7 @@ class Loader(object):
for filename in os.listdir(root_path): for filename in os.listdir(root_path):
path = os.path.join(root_path, filename) path = os.path.join(root_path, filename)
if os.path.isdir(path) and filename[:2] != '__': if os.path.isdir(path) and filename[:2] != '__':
if u'__init__.py' in os.listdir(path): if six.u('__init__.py') in os.listdir(path):
new_base_path = ''.join(s + '.' for s in base_path) + filename new_base_path = ''.join(s + '.' for s in base_path) + filename
self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path) self.paths[new_base_path.replace('.', '_')] = (priority, new_base_path, path)
+5 -4
View File
@@ -1,6 +1,7 @@
import logging import logging
import re import re
class CPLog(object): class CPLog(object):
context = '' context = ''
@@ -37,7 +38,7 @@ class CPLog(object):
def safeMessage(self, msg, replace_tuple = ()): def safeMessage(self, msg, replace_tuple = ()):
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss, toUnicode
msg = ss(msg) msg = ss(msg)
@@ -49,8 +50,8 @@ class CPLog(object):
msg = msg % tuple([ss(x) for x in list(replace_tuple)]) msg = msg % tuple([ss(x) for x in list(replace_tuple)])
else: else:
msg = msg % ss(replace_tuple) msg = msg % ss(replace_tuple)
except Exception, e: except Exception as e:
self.logger.error(u'Failed encoding stuff to log "%s": %s' % (msg, e)) self.logger.error('Failed encoding stuff to log "%s": %s' % (msg, e))
if not Env.get('dev'): if not Env.get('dev'):
@@ -66,4 +67,4 @@ class CPLog(object):
except: except:
pass pass
return msg return toUnicode(msg)
+28 -11
View File
@@ -1,8 +1,11 @@
from couchpotato import get_session import traceback
from couchpotato import get_session, CPLog
from couchpotato.core.event import addEvent, fireEventAsync, fireEvent from couchpotato.core.event import addEvent, fireEventAsync, fireEvent
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Media
log = CPLog(__name__)
class MediaBase(Plugin): class MediaBase(Plugin):
@@ -10,8 +13,8 @@ class MediaBase(Plugin):
default_dict = { default_dict = {
'profile': {'types': {'quality': {}}}, 'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}, 'files':{}, 'info': {}}, 'releases': {'status': {}, 'quality': {}, 'files': {}, 'info': {}},
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files': {}},
'files': {}, 'files': {},
'status': {}, 'status': {},
'category': {}, 'category': {},
@@ -26,19 +29,33 @@ class MediaBase(Plugin):
def createOnComplete(self, id): def createOnComplete(self, id):
def onComplete(): def onComplete():
db = get_session() try:
media = db.query(Media).filter_by(id = id).first() db = get_session()
fireEventAsync('%s.searcher.single' % media.type, media.to_dict(self.default_dict), on_complete = self.createNotifyFront(id)) media = db.query(Media).filter_by(id = id).first()
db.expire_all() media_dict = media.to_dict(self.default_dict)
event_name = '%s.searcher.single' % media.type
fireEvent(event_name, media_dict, on_complete = self.createNotifyFront(id))
except:
log.error('Failed creating onComplete: %s', traceback.format_exc())
finally:
db.close()
return onComplete return onComplete
def createNotifyFront(self, media_id): def createNotifyFront(self, media_id):
def notifyFront(): def notifyFront():
db = get_session() try:
media = db.query(Media).filter_by(id = media_id).first() db = get_session()
fireEvent('notify.frontend', type = '%s.update.%s' % (media.type, media.id), data = media.to_dict(self.default_dict)) media = db.query(Media).filter_by(id = media_id).first()
db.expire_all() media_dict = media.to_dict(self.default_dict)
event_name = '%s.update' % media.type
fireEvent('notify.frontend', type = event_name, data = media_dict)
except:
log.error('Failed creating onComplete: %s', traceback.format_exc())
finally:
db.close()
return notifyFront return notifyFront
@@ -1,5 +1,6 @@
from .main import MediaPlugin from .main import MediaPlugin
def start(): def start():
return MediaPlugin() return MediaPlugin()
+448 -17
View File
@@ -1,10 +1,16 @@
from couchpotato import get_session import traceback
from couchpotato import get_session, tryInt
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import mergeDicts, splitString, getImdb, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media import MediaBase from couchpotato.core.media import MediaBase
from couchpotato.core.settings.model import Media from couchpotato.core.settings.model import Library, LibraryTitle, Release, \
Media
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
log = CPLog(__name__) log = CPLog(__name__)
@@ -20,30 +26,455 @@ class MediaPlugin(MediaBase):
} }
}) })
addEvent('app.load', self.addSingleRefresh) addApiView('media.list', self.listView, docs = {
'desc': 'List media',
'params': {
'type': {'type': 'string', 'desc': 'Media type to filter on.'},
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'media': array, media found,
}"""}
})
addApiView('media.get', self.getView, docs = {
'desc': 'Get media by id',
'params': {
'id': {'desc': 'The id of the media'},
}
})
addApiView('media.delete', self.deleteView, docs = {
'desc': 'Delete a media from the wanted list',
'params': {
'id': {'desc': 'Media ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete media from this page', 'type': 'string: all (default), wanted, manage'},
}
})
addApiView('media.available_chars', self.charView)
addEvent('app.load', self.addSingleRefreshView)
addEvent('app.load', self.addSingleListView)
addEvent('app.load', self.addSingleCharView)
addEvent('app.load', self.addSingleDeleteView)
addEvent('media.get', self.get)
addEvent('media.list', self.list)
addEvent('media.delete', self.delete)
addEvent('media.restatus', self.restatus)
def refresh(self, id = '', **kwargs): def refresh(self, id = '', **kwargs):
db = get_session() handlers = []
ids = splitString(id)
for x in splitString(id): for x in ids:
media = db.query(Media).filter_by(id = x).first()
if media: refresh_handler = self.createRefreshHandler(x)
# Get current selected title if refresh_handler:
default_title = '' handlers.append(refresh_handler)
for title in media.library.titles:
if title.default: default_title = title.title
fireEvent('notify.frontend', type = '%s.busy.%s' % (media.type, x), data = True) fireEvent('notify.frontend', type = 'media.busy', data = {'id': [tryInt(x) for x in ids]})
fireEventAsync('library.update.%s' % media.type, identifier = media.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(x)) fireEventAsync('schedule.queue', handlers = handlers)
db.expire_all()
return { return {
'success': True, 'success': True,
} }
def addSingleRefresh(self): def createRefreshHandler(self, id):
db = get_session()
media = db.query(Media).filter_by(id = id).first()
if media:
default_title = getTitle(media.library)
identifier = media.library.identifier
event = 'library.update.%s' % media.type
def handler():
fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id))
if handler:
return handler
def addSingleRefreshView(self):
for media_type in fireEvent('media.types', merge = True): for media_type in fireEvent('media.types', merge = True):
addApiView('%s.refresh' % media_type, self.refresh) addApiView('%s.refresh' % media_type, self.refresh)
def get(self, media_id):
db = get_session()
imdb_id = getImdb(str(media_id))
if imdb_id:
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
else:
m = db.query(Media).filter_by(id = media_id).first()
results = None
if m:
results = m.to_dict(self.default_dict)
return results
def getView(self, id = None, **kwargs):
media = self.get(id) if id else None
return {
'success': media is not None,
'media': media,
}
def list(self, types = None, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
db = get_session()
# Make a list from string
if status and not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
# query movie ids
q = db.query(Media) \
.with_entities(Media.id) \
.group_by(Media.id)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.join(Media.releases)
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Media.library, Library.titles) \
.filter(LibraryTitle.default == True)
# Add search filters
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
if starts_with in ascii_lowercase:
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
else:
ignore = []
for letter in ascii_lowercase:
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
filter_or.append(not_(or_(*ignore)))
if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
if len(filter_or) > 0:
q = q.filter(or_(*filter_or))
total_count = q.count()
if total_count == 0:
return 0, []
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
# Get all media_ids in sorted order
media_ids = [m.id for m in q.all()]
# List release statuses
releases = db.query(Release) \
.filter(Release.movie_id.in_(media_ids)) \
.all()
release_statuses = dict((m, set()) for m in media_ids)
releases_count = dict((m, 0) for m in media_ids)
for release in releases:
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.movie_id] += 1
# Get main movie data
q2 = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
q2 = q2.filter(Media.id.in_(media_ids))
results = q2.all()
# Create dict by movie id
movie_dict = {}
for movie in results:
movie_dict[movie.id] = movie
# List movies based on media_ids order
movies = []
for media_id in media_ids:
releases = []
for r in release_statuses.get(media_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[media_id].to_dict({
'library': {'titles': {}, 'files': {}},
'files': {},
}), {
'releases': releases,
'releases_count': releases_count.get(media_id),
}))
return total_count, movies
def listView(self, **kwargs):
types = splitString(kwargs.get('types'))
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list(
types = types,
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
def addSingleListView(self):
for media_type in fireEvent('media.types', merge = True):
def tempList(*args, **kwargs):
return self.listView(types = media_type, *args, **kwargs)
addApiView('%s.list' % media_type, tempList)
def availableChars(self, types = None, status = None, release_status = None):
types = types or []
status = status or []
release_status = release_status or []
db = get_session()
# Make a list from string
if not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
if types and not isinstance(types, (list, tuple)):
types = [types]
q = db.query(Media)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.join(Media.releases) \
.filter(Release.status_id.in_(statuses))
# Filter on type
if types and len(types) > 0:
try: q = q.filter(Media.type.in_(types))
except: pass
q = q.join(Library, LibraryTitle) \
.with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
titles = q.all()
chars = set()
for title in titles:
try:
char = title[0][0]
char = char if char in ascii_lowercase else '#'
chars.add(str(char))
except:
log.error('Failed getting title for %s', title.libraries_id)
if len(chars) == 25:
break
return ''.join(sorted(chars))
def charView(self, **kwargs):
type = splitString(kwargs.get('type', 'movie'))
status = splitString(kwargs.get('status', None))
release_status = splitString(kwargs.get('release_status', None))
chars = self.availableChars(type, status, release_status)
return {
'success': True,
'empty': len(chars) == 0,
'chars': chars,
}
def addSingleCharView(self):
for media_type in fireEvent('media.types', merge = True):
def tempChar(*args, **kwargs):
return self.charView(types = media_type, *args, **kwargs)
addApiView('%s.available_chars' % media_type, tempChar)
def delete(self, media_id, delete_from = None):
try:
db = get_session()
media = db.query(Media).filter_by(id = media_id).first()
if media:
deleted = False
if delete_from == 'all':
db.delete(media)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
total_releases = len(media.releases)
total_deleted = 0
new_movie_status = None
for release in media.releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage':
if release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit()
if total_releases == total_deleted:
db.delete(media)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
media.profile_id = None
media.status_id = new_status.get('id')
db.commit()
else:
fireEvent('media.restatus', media.id, single = True)
if deleted:
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict())
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return True
def deleteView(self, id = '', **kwargs):
ids = splitString(id)
for media_id in ids:
self.delete(media_id, delete_from = kwargs.get('delete_from', 'all'))
return {
'success': True,
}
def addSingleDeleteView(self):
for media_type in fireEvent('media.types', merge = True):
def tempDelete(*args, **kwargs):
return self.deleteView(types = media_type, *args, **kwargs)
addApiView('%s.delete' % media_type, tempDelete)
def restatus(self, media_id):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
try:
db = get_session()
m = db.query(Media).filter_by(id = media_id).first()
if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile:
m.status_id = done_status.get('id')
else:
move_to_wanted = True
for t in m.profile.types:
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit()
return True
except:
log.error('Failed restatus: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
@@ -1,5 +1,6 @@
from .main import Search from .main import Search
def start(): def start():
return Search() return Search()
@@ -1,5 +1,6 @@
from .main import Searcher from .main import Searcher
def start(): def start():
return Searcher() return Searcher()
@@ -12,7 +12,6 @@ class SearcherBase(Plugin):
def __init__(self): def __init__(self):
super(SearcherBase, self).__init__() super(SearcherBase, self).__init__()
addEvent('searcher.progress', self.getProgress) addEvent('searcher.progress', self.getProgress)
addEvent('%s.searcher.progress' % self.getType(), self.getProgress) addEvent('%s.searcher.progress' % self.getType(), self.getProgress)
@@ -26,9 +25,8 @@ class SearcherBase(Plugin):
_type = self.getType() _type = self.getType()
def setCrons(): def setCrons():
fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll, fireEvent('schedule.cron', '%s.searcher.all' % _type, self.searchAll,
day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
addEvent('app.load', setCrons) addEvent('app.load', setCrons)
addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons) addEvent('setting.save.%s_searcher.cron_day.after' % _type, setCrons)
@@ -1,7 +1,7 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.variable import splitString, removeEmpty, removeDuplicate
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media._base.searcher.base import SearcherBase from couchpotato.core.media._base.searcher.base import SearcherBase
import datetime import datetime
@@ -107,10 +107,10 @@ class Searcher(SearcherBase):
# Hack for older movies that don't contain quality tag # Hack for older movies that don't contain quality tag
year_name = fireEvent('scanner.name_year', name, single = True) year_name = fireEvent('scanner.name_year', name, single = True)
if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None): if len(found) == 0 and movie_year < datetime.datetime.now().year - 3 and not year_name.get('year', None):
if size > 3000: # Assume dvdr if size > 3000: # Assume dvdr
log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size) log.info('Quality was missing in name, assuming it\'s a DVD-R based on the size: %s', size)
found['dvdr'] = True found['dvdr'] = True
else: # Assume dvdrip else: # Assume dvdrip
log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size) log.info('Quality was missing in name, assuming it\'s a DVD-Rip based on the size: %s', size)
found['dvdrip'] = True found['dvdrip'] = True
@@ -150,12 +150,12 @@ class Searcher(SearcherBase):
try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip()) try: check_names.append(max(re.findall(r'[^[]*\[([^]]*)\]', check_name), key = len).strip())
except: pass except: pass
for check_name in list(set(check_names)): for check_name in removeDuplicate(check_names):
check_movie = fireEvent('scanner.name_year', check_name, single = True) check_movie = fireEvent('scanner.name_year', check_name, single = True)
try: try:
check_words = filter(None, re.split('\W+', check_movie.get('name', ''))) check_words = removeEmpty(re.split('\W+', check_movie.get('name', '')))
movie_words = filter(None, re.split('\W+', simplifyString(movie_name))) movie_words = removeEmpty(re.split('\W+', simplifyString(movie_name)))
if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0: if len(check_words) > 0 and len(movie_words) > 0 and len(list(set(check_words) - set(movie_words))) == 0:
return True return True
@@ -173,7 +173,7 @@ class Searcher(SearcherBase):
# Make sure it has required words # Make sure it has required words
required_words = splitString(self.conf('required_words', section = 'searcher').lower()) required_words = splitString(self.conf('required_words', section = 'searcher').lower())
try: required_words = list(set(required_words + splitString(media['category']['required'].lower()))) try: required_words = removeDuplicate(required_words + splitString(media['category']['required'].lower()))
except: pass except: pass
req_match = 0 req_match = 0
@@ -187,7 +187,7 @@ class Searcher(SearcherBase):
# Ignore releases # Ignore releases
ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower()) ignored_words = splitString(self.conf('ignored_words', section = 'searcher').lower())
try: ignored_words = list(set(ignored_words + splitString(media['category']['ignored'].lower()))) try: ignored_words = removeDuplicate(ignored_words + splitString(media['category']['ignored'].lower()))
except: pass except: pass
ignored_match = 0 ignored_match = 0
@@ -1,5 +1,6 @@
from .main import MovieBase from .main import MovieBase
def start(): def start():
return MovieBase() return MovieBase()
+110 -446
View File
@@ -1,16 +1,12 @@
import traceback
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, fireEventAsync, addEvent from couchpotato.core.event import fireEvent, fireEventAsync, addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import getImdb, splitString, tryInt, \ from couchpotato.core.helpers.variable import splitString, tryInt, getTitle
mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.media.movie import MovieTypeBase from couchpotato.core.media.movie import MovieTypeBase
from couchpotato.core.settings.model import Library, LibraryTitle, Media, \ from couchpotato.core.settings.model import Media
Release
from sqlalchemy.orm import joinedload_all
from sqlalchemy.sql.expression import or_, asc, not_, desc
from string import ascii_lowercase
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@@ -26,33 +22,12 @@ class MovieBase(MovieTypeBase):
super(MovieBase, self).__init__() super(MovieBase, self).__init__()
self.initType() self.initType()
addApiView('movie.list', self.listView, docs = {
'desc': 'List movies in wanted list',
'params': {
'status': {'type': 'array or csv', 'desc': 'Filter movie by status. Example:"active,done"'},
'release_status': {'type': 'array or csv', 'desc': 'Filter movie by status of its releases. Example:"snatched,available"'},
'limit_offset': {'desc': 'Limit and offset the movie list. Examples: "50" or "50,30"'},
'starts_with': {'desc': 'Starts with these characters. Example: "a" returns all movies starting with the letter "a"'},
'search': {'desc': 'Search movie title'},
},
'return': {'type': 'object', 'example': """{
'success': True,
'empty': bool, any movies returned or not,
'movies': array, movies found,
}"""}
})
addApiView('movie.get', self.getView, docs = {
'desc': 'Get a movie by id',
'params': {
'id': {'desc': 'The id of the movie'},
}
})
addApiView('movie.available_chars', self.charView)
addApiView('movie.add', self.addView, docs = { addApiView('movie.add', self.addView, docs = {
'desc': 'Add new movie to the wanted list', 'desc': 'Add new movie to the wanted list',
'params': { 'params': {
'identifier': {'desc': 'IMDB id of the movie your want to add.'}, 'identifier': {'desc': 'IMDB id of the movie your want to add.'},
'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'}, 'profile_id': {'desc': 'ID of quality profile you want the add the movie in. If empty will use the default profile.'},
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, 'title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
} }
}) })
@@ -61,258 +36,12 @@ class MovieBase(MovieTypeBase):
'params': { 'params': {
'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'}, 'id': {'desc': 'Movie ID(s) you want to edit.', 'type': 'int (comma separated)'},
'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'}, 'profile_id': {'desc': 'ID of quality profile you want the edit the movie to.'},
'category_id': {'desc': 'ID of category you want the add the movie in. If empty will use no category.'},
'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'}, 'default_title': {'desc': 'Movie title to use for searches. Has to be one of the titles returned by movie.search.'},
} }
}) })
addApiView('movie.delete', self.deleteView, docs = {
'desc': 'Delete a movie from the wanted list',
'params': {
'id': {'desc': 'Movie ID(s) you want to delete.', 'type': 'int (comma separated)'},
'delete_from': {'desc': 'Delete movie from this page', 'type': 'string: all (default), wanted, manage'},
}
})
addEvent('movie.add', self.add) addEvent('movie.add', self.add)
addEvent('movie.delete', self.delete)
addEvent('movie.get', self.get)
addEvent('movie.list', self.list)
addEvent('movie.restatus', self.restatus)
def getView(self, id = None, **kwargs):
movie = self.get(id) if id else None
return {
'success': movie is not None,
'movie': movie,
}
def get(self, movie_id):
db = get_session()
imdb_id = getImdb(str(movie_id))
if imdb_id:
m = db.query(Media).filter(Media.library.has(identifier = imdb_id)).first()
else:
m = db.query(Media).filter_by(id = movie_id).first()
results = None
if m:
results = m.to_dict(self.default_dict)
db.expire_all()
return results
def list(self, status = None, release_status = None, limit_offset = None, starts_with = None, search = None, order = None):
db = get_session()
# Make a list from string
if status and not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
# query movie ids
q = db.query(Media) \
.with_entities(Media.id) \
.group_by(Media.id)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
q = q.join(Media.releases)
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Release.status_id.in_(statuses))
# Only join when searching / ordering
if starts_with or search or order != 'release_order':
q = q.join(Media.library, Library.titles) \
.filter(LibraryTitle.default == True)
# Add search filters
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
if starts_with in ascii_lowercase:
filter_or.append(LibraryTitle.simple_title.startswith(starts_with))
else:
ignore = []
for letter in ascii_lowercase:
ignore.append(LibraryTitle.simple_title.startswith(toUnicode(letter)))
filter_or.append(not_(or_(*ignore)))
if search:
filter_or.append(LibraryTitle.simple_title.like('%%' + search + '%%'))
if len(filter_or) > 0:
q = q.filter(or_(*filter_or))
total_count = q.count()
if total_count == 0:
return 0, []
if order == 'release_order':
q = q.order_by(desc(Release.last_edit))
else:
q = q.order_by(asc(LibraryTitle.simple_title))
if limit_offset:
splt = splitString(limit_offset) if isinstance(limit_offset, (str, unicode)) else limit_offset
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
# Get all movie_ids in sorted order
movie_ids = [m.id for m in q.all()]
# List release statuses
releases = db.query(Release) \
.filter(Release.movie_id.in_(movie_ids)) \
.all()
release_statuses = dict((m, set()) for m in movie_ids)
releases_count = dict((m, 0) for m in movie_ids)
for release in releases:
release_statuses[release.movie_id].add('%d,%d' % (release.status_id, release.quality_id))
releases_count[release.movie_id] += 1
# Get main movie data
q2 = db.query(Media) \
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files'))
q2 = q2.filter(Media.id.in_(movie_ids))
results = q2.all()
# Create dict by movie id
movie_dict = {}
for movie in results:
movie_dict[movie.id] = movie
# List movies based on movie_ids order
movies = []
for movie_id in movie_ids:
releases = []
for r in release_statuses.get(movie_id):
x = splitString(r)
releases.append({'status_id': x[0], 'quality_id': x[1]})
# Merge releases with movie dict
movies.append(mergeDicts(movie_dict[movie_id].to_dict({
'library': {'titles': {}, 'files':{}},
'files': {},
}), {
'releases': releases,
'releases_count': releases_count.get(movie_id),
}))
db.expire_all()
return total_count, movies
def availableChars(self, status = None, release_status = None):
status = status or []
release_status = release_status or []
db = get_session()
# Make a list from string
if not isinstance(status, (list, tuple)):
status = [status]
if release_status and not isinstance(release_status, (list, tuple)):
release_status = [release_status]
q = db.query(Media)
# Filter on movie status
if status and len(status) > 0:
statuses = fireEvent('status.get', status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.filter(Media.status_id.in_(statuses))
# Filter on release status
if release_status and len(release_status) > 0:
statuses = fireEvent('status.get', release_status, single = len(release_status) > 1)
statuses = [s.get('id') for s in statuses]
q = q.join(Media.releases) \
.filter(Release.status_id.in_(statuses))
q = q.join(Library, LibraryTitle) \
.with_entities(LibraryTitle.simple_title) \
.filter(LibraryTitle.default == True)
titles = q.all()
chars = set()
for title in titles:
try:
char = title[0][0]
char = char if char in ascii_lowercase else '#'
chars.add(str(char))
except:
log.error('Failed getting title for %s', title.libraries_id)
if len(chars) == 25:
break
db.expire_all()
return ''.join(sorted(chars))
def listView(self, **kwargs):
status = splitString(kwargs.get('status'))
release_status = splitString(kwargs.get('release_status'))
limit_offset = kwargs.get('limit_offset')
starts_with = kwargs.get('starts_with')
search = kwargs.get('search')
order = kwargs.get('order')
total_movies, movies = self.list(
status = status,
release_status = release_status,
limit_offset = limit_offset,
starts_with = starts_with,
search = search,
order = order
)
return {
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
}
def charView(self, **kwargs):
status = splitString(kwargs.get('status', None))
release_status = splitString(kwargs.get('release_status', None))
chars = self.availableChars(status, release_status)
return {
'success': True,
'empty': len(chars) == 0,
'chars': chars,
}
def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None): def add(self, params = None, force_readd = True, search_after = True, update_library = False, status_id = None):
if not params: params = {} if not params: params = {}
@@ -333,7 +62,6 @@ class MovieBase(MovieTypeBase):
except: except:
pass pass
library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library) library = fireEvent('library.add.movie', single = True, attrs = params, update_after = update_library)
# Status # Status
@@ -343,68 +71,81 @@ class MovieBase(MovieTypeBase):
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
cat_id = params.get('category_id') cat_id = params.get('category_id')
db = get_session() try:
m = db.query(Media).filter_by(library_id = library.get('id')).first() db = get_session()
added = True m = db.query(Media).filter_by(library_id = library.get('id')).first()
do_search = False added = True
search_after = search_after and self.conf('search_on_add', section = 'moviesearcher') do_search = False
if not m: search_after = search_after and self.conf('search_on_add', section = 'moviesearcher')
m = Media( if not m:
library_id = library.get('id'), m = Media(
profile_id = params.get('profile_id', default_profile.get('id')), library_id = library.get('id'),
status_id = status_id if status_id else status_active.get('id'), profile_id = params.get('profile_id', default_profile.get('id')),
category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None, status_id = status_id if status_id else status_active.get('id'),
) category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else None,
db.add(m) )
db.commit() db.add(m)
onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd:
# Clean snatched history
for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
fireEvent('release.delete', release.id, single = True)
m.profile_id = params.get('profile_id', default_profile.get('id'))
m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None)
else:
log.debug('Movie already exists, not updating: %s', params)
added = False
if force_readd:
m.status_id = status_id if status_id else status_active.get('id')
m.last_edit = int(time.time())
do_search = True
db.commit()
# Remove releases
available_status = fireEvent('status.get', 'available', single = True)
for rel in m.releases:
if rel.status_id is available_status.get('id'):
db.delete(rel)
db.commit() db.commit()
movie_dict = m.to_dict(self.default_dict) onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
if do_search and search_after: fireEventAsync('library.update.movie', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
onComplete = self.createOnComplete(m.id) search_after = False
onComplete() elif force_readd:
if added: # Clean snatched history
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')) for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id')
else:
fireEvent('release.delete', release.id, single = True)
db.expire_all() m.profile_id = params.get('profile_id', default_profile.get('id'))
return movie_dict m.category_id = tryInt(cat_id) if cat_id is not None and tryInt(cat_id) > 0 else (m.category_id or None)
else:
log.debug('Movie already exists, not updating: %s', params)
added = False
if force_readd:
m.status_id = status_id if status_id else status_active.get('id')
m.last_edit = int(time.time())
do_search = True
db.commit()
# Remove releases
available_status = fireEvent('status.get', 'available', single = True)
for rel in m.releases:
if rel.status_id is available_status.get('id'):
db.delete(rel)
db.commit()
movie_dict = m.to_dict(self.default_dict)
if do_search and search_after:
onComplete = self.createOnComplete(m.id)
onComplete()
if added:
if params.get('title'):
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '')
else:
title = getTitle(m.library)
if title:
message = 'Successfully added "%s" to your wanted list.' % title
else:
message = 'Succesfully added to your wanted list.'
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = message)
return movie_dict
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def addView(self, **kwargs): def addView(self, **kwargs):
add_dict = self.add(params = kwargs) add_dict = self.add(params = kwargs)
@@ -416,128 +157,51 @@ class MovieBase(MovieTypeBase):
def edit(self, id = '', **kwargs): def edit(self, id = '', **kwargs):
db = get_session() try:
db = get_session()
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ids = splitString(id) ids = splitString(id)
for movie_id in ids: for media_id in ids:
m = db.query(Media).filter_by(id = movie_id).first() m = db.query(Media).filter_by(id = media_id).first()
if not m: if not m:
continue continue
m.profile_id = kwargs.get('profile_id') m.profile_id = kwargs.get('profile_id')
cat_id = kwargs.get('category_id') cat_id = kwargs.get('category_id')
if cat_id is not None: if cat_id is not None:
m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None m.category_id = tryInt(cat_id) if tryInt(cat_id) > 0 else None
# Remove releases # Remove releases
for rel in m.releases: for rel in m.releases:
if rel.status_id is available_status.get('id'): if rel.status_id is available_status.get('id'):
db.delete(rel) db.delete(rel)
db.commit() db.commit()
# Default title # Default title
if kwargs.get('default_title'): if kwargs.get('default_title'):
for title in m.library.titles: for title in m.library.titles:
title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower() title.default = toUnicode(kwargs.get('default_title', '')).lower() == toUnicode(title.title).lower()
db.commit()
fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.default_dict)
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
db.expire_all()
return {
'success': True,
}
def deleteView(self, id = '', **kwargs):
ids = splitString(id)
for movie_id in ids:
self.delete(movie_id, delete_from = kwargs.get('delete_from', 'all'))
return {
'success': True,
}
def delete(self, movie_id, delete_from = None):
db = get_session()
movie = db.query(Media).filter_by(id = movie_id).first()
if movie:
deleted = False
if delete_from == 'all':
db.delete(movie)
db.commit()
deleted = True
else:
done_status = fireEvent('status.get', 'done', single = True)
total_releases = len(movie.releases)
total_deleted = 0
new_movie_status = None
for release in movie.releases:
if delete_from in ['wanted', 'snatched', 'late']:
if release.status_id != done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'done'
elif delete_from == 'manage':
if release.status_id == done_status.get('id'):
db.delete(release)
total_deleted += 1
new_movie_status = 'active'
db.commit() db.commit()
if total_releases == total_deleted: fireEvent('media.restatus', m.id)
db.delete(movie)
db.commit()
deleted = True
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None
movie.status_id = new_status.get('id')
db.commit()
else:
fireEvent('movie.restatus', movie.id, single = True)
if deleted: movie_dict = m.to_dict(self.default_dict)
fireEvent('notify.frontend', type = 'movie.deleted', data = movie.to_dict()) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
db.expire_all() return {
return True 'success': True,
}
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def restatus(self, movie_id): return {
'success': False,
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) }
db = get_session()
m = db.query(Media).filter_by(id = movie_id).first()
if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False
log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile:
m.status_id = done_status.get('id')
else:
move_to_wanted = True
for t in m.profile.types:
for release in m.releases:
if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit()
return True
@@ -52,8 +52,8 @@ var MovieList = new Class({
self.getMovies(); self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self)) App.on('movie.added', self.movieAdded.bind(self))
App.addEvent('movie.deleted', self.movieDeleted.bind(self)) App.on('movie.deleted', self.movieDeleted.bind(self))
}, },
movieDeleted: function(notification){ movieDeleted: function(notification){
@@ -65,6 +65,7 @@ var MovieList = new Class({
movie.destroy(); movie.destroy();
delete self.movies_added[notification.data.id]; delete self.movies_added[notification.data.id];
self.setCounter(self.counter_count-1); self.setCounter(self.counter_count-1);
self.total_movies--;
} }
}) })
} }
@@ -75,6 +76,7 @@ var MovieList = new Class({
movieAdded: function(notification){ movieAdded: function(notification){
var self = this; var self = this;
self.fireEvent('movieAdded', notification);
if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){ if(self.options.add_new && !self.movies_added[notification.data.id] && notification.data.status.identifier == self.options.status){
window.scroll(0,0); window.scroll(0,0);
self.createMovie(notification.data, 'top'); self.createMovie(notification.data, 'top');
@@ -279,7 +281,7 @@ var MovieList = new Class({
// Get available chars and highlight // Get available chars and highlight
if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible())) if(!available_chars && (self.navigation.isDisplayed() || self.navigation.isVisible()))
Api.request('movie.available_chars', { Api.request('media.available_chars', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status 'status': self.options.status
}, self.filter), }, self.filter),
@@ -370,7 +372,7 @@ var MovieList = new Class({
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
this.set('text', 'Deleting..') this.set('text', 'Deleting..')
Api.request('movie.delete', { Api.request('media.delete', {
'data': { 'data': {
'id': ids.join(','), 'id': ids.join(','),
'delete_from': self.options.identifier 'delete_from': self.options.identifier
@@ -390,6 +392,7 @@ var MovieList = new Class({
self.movies.erase(movie); self.movies.erase(movie);
movie.destroy(); movie.destroy();
self.setCounter(self.counter_count-1); self.setCounter(self.counter_count-1);
self.total_movies--;
}); });
self.calculateSelected(); self.calculateSelected();
@@ -547,8 +550,9 @@ var MovieList = new Class({
} }
Api.request(self.options.api_call || 'movie.list', { Api.request(self.options.api_call || 'media.list', {
'data': Object.merge({ 'data': Object.merge({
'type': 'movie',
'status': self.options.status, 'status': self.options.status,
'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null 'limit_offset': self.options.limit ? self.options.limit + ',' + self.offset : null
}, self.filter), }, self.filter),
@@ -126,7 +126,9 @@ MA.Release = new Class({
else else
self.showHelper(); self.showHelper();
App.addEvent('movie.searcher.ended.'+self.movie.data.id, function(notification){ App.on('movie.searcher.ended', function(notification){
if(self.movie.data.id != notification.data.id) return;
self.releases = null; self.releases = null;
if(self.options_container){ if(self.options_container){
self.options_container.destroy(); self.options_container.destroy();
@@ -250,12 +252,14 @@ MA.Release = new Class({
else if(!self.next_release && status.identifier == 'available'){ else if(!self.next_release && status.identifier == 'available'){
self.next_release = release; self.next_release = release;
} }
var update_handle = function(notification) { var update_handle = function(notification) {
var q = self.movie.quality.getElement('.q_id' + release.quality_id), if(notification.data.id != release.id) return;
var q = self.movie.quality.getElement('.q_id' + release.quality_id),
status = Status.get(release.status_id), status = Status.get(release.status_id),
new_status = Status.get(notification.data); new_status = Status.get(notification.data.status_id);
release.status_id = new_status.id release.status_id = new_status.id
release.el.set('class', 'item ' + new_status.identifier); release.el.set('class', 'item ' + new_status.identifier);
@@ -272,7 +276,7 @@ MA.Release = new Class({
} }
} }
App.addEvent('release.update_status.' + release.id, update_handle); App.on('release.update_status', update_handle);
}); });
@@ -285,7 +289,7 @@ MA.Release = new Class({
if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){ if(self.next_release || (self.last_release && ['ignored', 'failed'].indexOf(self.last_release.status.identifier) === false)){
self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top'); self.trynext_container = new Element('div.buttons.try_container').inject(self.release_container, 'top');
var nr = self.next_release, var nr = self.next_release,
lr = self.last_release; lr = self.last_release;
@@ -427,7 +431,7 @@ MA.Release = new Class({
markMovieDone: function(){ markMovieDone: function(){
var self = this; var self = this;
Api.request('movie.delete', { Api.request('media.delete', {
'data': { 'data': {
'id': self.movie.get('id'), 'id': self.movie.get('id'),
'delete_from': 'wanted' 'delete_from': 'wanted'
@@ -446,7 +450,7 @@ MA.Release = new Class({
}, },
tryNextRelease: function(movie_id){ tryNextRelease: function(){
var self = this; var self = this;
Api.request('movie.searcher.try_next', { Api.request('movie.searcher.try_next', {
@@ -817,7 +821,7 @@ MA.Delete = new Class({
self.callChain(); self.callChain();
}, },
function(){ function(){
Api.request('movie.delete', { Api.request('media.delete', {
'data': { 'data': {
'id': self.movie.get('id'), 'id': self.movie.get('id'),
'delete_from': self.movie.list.options.identifier 'delete_from': self.movie.list.options.identifier
@@ -1036,7 +1036,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
width: 85%; width: 85%;
direction: rtl; direction: ltr;
vertical-align: middle; vertical-align: middle;
} }
@@ -23,23 +23,49 @@ var Movie = new Class({
addEvents: function(){ addEvents: function(){
var self = this; var self = this;
App.addEvent('movie.update.'+self.data.id, function(notification){ self.global_events = {}
// Do refresh with new data
self.global_events['movie.update'] = function(notification){
if(self.data.id != notification.data.id) return;
self.busy(false); self.busy(false);
self.removeView(); self.removeView();
self.update.delay(2000, self, notification); self.update.delay(2000, self, notification);
}); }
App.on('movie.update', self.global_events['movie.update']);
['movie.busy', 'movie.searcher.started'].each(function(listener){ // Add spinner on load / search
App.addEvent(listener+'.'+self.data.id, function(notification){ ['media.busy', 'movie.searcher.started'].each(function(listener){
if(notification.data) self.global_events[listener] = function(notification){
self.busy(true) if(notification.data && (self.data.id == notification.data.id || (typeOf(notification.data.id) == 'array' && notification.data.id.indexOf(self.data.id) > -1)))
}); self.busy(true);
}
App.on(listener, self.global_events[listener]);
}) })
App.addEvent('movie.searcher.ended.'+self.data.id, function(notification){ // Remove spinner
if(notification.data) self.global_events['movie.searcher.ended'] = function(notification){
if(notification.data && self.data.id == notification.data.id)
self.busy(false) self.busy(false)
}); }
App.on('movie.searcher.ended', self.global_events['movie.searcher.ended']);
// Reload when releases have updated
self.global_events['release.update_status'] = function(notification){
var data = notification.data
if(data && self.data.id == data.movie_id){
if(!self.data.releases)
self.data.releases = [];
self.data.releases.push({'quality_id': data.quality_id, 'status_id': data.status_id});
self.updateReleases();
}
}
App.on('release.update_status', self.global_events['release.update_status']);
}, },
destroy: function(){ destroy: function(){
@@ -52,10 +78,9 @@ var Movie = new Class({
self.list.checkIfEmpty(); self.list.checkIfEmpty();
// Remove events // Remove events
App.removeEvents('movie.update.'+self.data.id); Object.each(self.global_events, function(handle, listener){
['movie.busy', 'movie.searcher.started'].each(function(listener){ App.off(listener, handle);
App.removeEvents(listener+'.'+self.data.id); });
})
}, },
busy: function(set_busy, timeout){ busy: function(set_busy, timeout){
@@ -179,21 +204,7 @@ var Movie = new Class({
}); });
// Add releases // Add releases
if(self.data.releases) self.updateReleases();
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id);
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id)
if (status && q && !q.hasClass(status.identifier)){
q.addClass(status.identifier);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
}
});
Object.each(self.options.actions, function(action, key){ Object.each(self.options.actions, function(action, key){
self.action[key.toLowerCase()] = action = new self.options.actions[key](self) self.action[key.toLowerCase()] = action = new self.options.actions[key](self)
@@ -203,6 +214,26 @@ var Movie = new Class({
}, },
updateReleases: function(){
var self = this;
if(!self.data.releases || self.data.releases.length == 0) return;
self.data.releases.each(function(release){
var q = self.quality.getElement('.q_id'+ release.quality_id),
status = Status.get(release.status_id);
if(!q && (status.identifier == 'snatched' || status.identifier == 'seeding' || status.identifier == 'done'))
var q = self.addQuality(release.quality_id)
if (status && q && !q.hasClass(status.identifier)){
q.addClass(status.identifier);
q.set('title', (q.get('title') ? q.get('title') : '') + ' status: '+ status.label)
}
});
},
addQuality: function(quality_id){ addQuality: function(quality_id){
var self = this; var self = this;
@@ -298,4 +329,4 @@ var Movie = new Class({
return this.el; return this.el;
} }
}); });
@@ -1,5 +1,6 @@
from .main import MovieLibraryPlugin from .main import MovieLibraryPlugin
def start(): def start():
return MovieLibraryPlugin() return MovieLibraryPlugin()
@@ -7,13 +7,14 @@ from couchpotato.core.settings.model import Library, LibraryTitle, File
from string import ascii_letters from string import ascii_letters
import time import time
import traceback import traceback
import six
log = CPLog(__name__) log = CPLog(__name__)
class MovieLibraryPlugin(LibraryBase): class MovieLibraryPlugin(LibraryBase):
default_dict = {'titles': {}, 'files':{}} default_dict = {'titles': {}, 'files': {}}
def __init__(self): def __init__(self):
addEvent('library.add.movie', self.add) addEvent('library.add.movie', self.add)
@@ -25,69 +26,70 @@ class MovieLibraryPlugin(LibraryBase):
primary_provider = attrs.get('primary_provider', 'imdb') primary_provider = attrs.get('primary_provider', 'imdb')
db = get_session() try:
db = get_session()
l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first() l = db.query(Library).filter_by(identifier = attrs.get('identifier')).first()
if not l: if not l:
status = fireEvent('status.get', 'needs_update', single = True) status = fireEvent('status.get', 'needs_update', single = True)
l = Library( l = Library(
year = attrs.get('year'), year = attrs.get('year'),
identifier = attrs.get('identifier'), identifier = attrs.get('identifier'),
plot = toUnicode(attrs.get('plot')), plot = toUnicode(attrs.get('plot')),
tagline = toUnicode(attrs.get('tagline')), tagline = toUnicode(attrs.get('tagline')),
status_id = status.get('id'), status_id = status.get('id'),
info = {} info = {}
) )
title = LibraryTitle( title = LibraryTitle(
title = toUnicode(attrs.get('title')), title = toUnicode(attrs.get('title')),
simple_title = self.simplifyTitle(attrs.get('title')), simple_title = self.simplifyTitle(attrs.get('title')),
) )
l.titles.append(title) l.titles.append(title)
db.add(l) db.add(l)
db.commit() db.commit()
# Update library info # Update library info
if update_after is not False: if update_after is not False:
handle = fireEventAsync if update_after is 'async' else fireEvent handle = fireEventAsync if update_after is 'async' else fireEvent
handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', ''))) handle('library.update.movie', identifier = l.identifier, default_title = toUnicode(attrs.get('title', '')))
library_dict = l.to_dict(self.default_dict) library_dict = l.to_dict(self.default_dict)
return library_dict
except:
log.error('Failed adding media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
db.expire_all() return {}
return library_dict
def update(self, identifier, default_title = '', force = False): def update(self, identifier, default_title = '', extended = False):
if self.shuttingDown(): if self.shuttingDown():
return return
db = get_session() try:
library = db.query(Library).filter_by(identifier = identifier).first() db = get_session()
done_status = fireEvent('status.get', 'done', single = True)
library_dict = None library = db.query(Library).filter_by(identifier = identifier).first()
if library: done_status = fireEvent('status.get', 'done', single = True)
library_dict = library.to_dict(self.default_dict)
do_update = True info = fireEvent('movie.info', merge = True, extended = extended, identifier = identifier)
info = fireEvent('movie.info', merge = True, identifier = identifier) # Don't need those here
try: del info['in_wanted']
except: pass
try: del info['in_library']
except: pass
# Don't need those here if not info or len(info) == 0:
try: del info['in_wanted'] log.error('Could not update, no movie info to work with: %s', identifier)
except: pass return False
try: del info['in_library']
except: pass
if not info or len(info) == 0: # Main info
log.error('Could not update, no movie info to work with: %s', identifier)
return False
# Main info
if do_update:
library.plot = toUnicode(info.get('plot', '')) library.plot = toUnicode(info.get('plot', ''))
library.tagline = toUnicode(info.get('tagline', '')) library.tagline = toUnicode(info.get('tagline', ''))
library.year = info.get('year', 0) library.year = info.get('year', 0)
@@ -102,6 +104,17 @@ class MovieLibraryPlugin(LibraryBase):
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s', titles) log.debug('Adding titles: %s', titles)
counter = 0 counter = 0
def_title = None
for title in titles:
if (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == six.u('') and toUnicode(titles[0]) == title):
def_title = toUnicode(title)
break
counter += 1
if not def_title:
def_title = toUnicode(titles[0])
for title in titles: for title in titles:
if not title: if not title:
continue continue
@@ -109,10 +122,9 @@ class MovieLibraryPlugin(LibraryBase):
t = LibraryTitle( t = LibraryTitle(
title = title, title = title,
simple_title = self.simplifyTitle(title), simple_title = self.simplifyTitle(title),
default = (len(default_title) == 0 and counter == 0) or len(titles) == 1 or title.lower() == toUnicode(default_title.lower()) or (toUnicode(default_title) == u'' and toUnicode(titles[0]) == title) default = title == def_title
) )
library.titles.append(t) library.titles.append(t)
counter += 1
db.commit() db.commit()
@@ -134,30 +146,43 @@ class MovieLibraryPlugin(LibraryBase):
break break
except: except:
log.debug('Failed to attach to library: %s', traceback.format_exc()) log.debug('Failed to attach to library: %s', traceback.format_exc())
db.rollback()
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
return library_dict
except:
log.error('Failed update media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
db.expire_all() return {}
return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
db = get_session() try:
library = db.query(Library).filter_by(identifier = identifier).first() db = get_session()
library = db.query(Library).filter_by(identifier = identifier).first()
if not library.info: if not library.info:
library_dict = self.update(identifier, force = True) library_dict = self.update(identifier)
dates = library_dict.get('info', {}).get('release_date') dates = library_dict.get('info', {}).get('release_date')
else: else:
dates = library.info.get('release_date') dates = library.info.get('release_date')
if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates: if dates and (dates.get('expires', 0) < time.time() or dates.get('expires', 0) > time.time() + (604800 * 4)) or not dates:
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
library.info.update({'release_date': dates }) library.info.update({'release_date': dates})
db.commit() db.commit()
db.expire_all() return dates
return dates except:
log.error('Failed updating release dates: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return {}
def simplifyTitle(self, title): def simplifyTitle(self, title):
@@ -1,6 +1,7 @@
from .main import MovieSearcher from .main import MovieSearcher
import random import random
def start(): def start():
return MovieSearcher() return MovieSearcher()
+35 -23
View File
@@ -73,10 +73,21 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
db = get_session() db = get_session()
movies = db.query(Media).filter( movies_raw = db.query(Media).filter(
Media.status.has(identifier = 'active') Media.status.has(identifier = 'active')
).all() ).all()
random.shuffle(movies)
random.shuffle(movies_raw)
movies = []
for m in movies_raw:
movies.append(m.to_dict({
'category': {},
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files': {}},
'files': {},
}))
self.in_progress = { self.in_progress = {
'total': len(movies), 'total': len(movies),
@@ -87,21 +98,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
search_protocols = fireEvent('searcher.protocols', single = True) search_protocols = fireEvent('searcher.protocols', single = True)
for movie in movies: for movie in movies:
movie_dict = movie.to_dict({
'category': {},
'profile': {'types': {'quality': {}}},
'releases': {'status': {}, 'quality': {}},
'library': {'titles': {}, 'files':{}},
'files': {},
})
try: try:
self.single(movie_dict, search_protocols) self.single(movie, search_protocols)
except IndexError: except IndexError:
log.error('Forcing library update for %s, if you see this often, please report: %s', (movie_dict['library']['identifier'], traceback.format_exc())) log.error('Forcing library update for %s, if you see this often, please report: %s', (movie['library']['identifier'], traceback.format_exc()))
fireEvent('library.update.movie', movie_dict['library']['identifier'], force = True) fireEvent('library.update.movie', movie['library']['identifier'])
except: except:
log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc())) log.error('Search failed for %s: %s', (movie['library']['identifier'], traceback.format_exc()))
self.in_progress['to_go'] -= 1 self.in_progress['to_go'] -= 1
@@ -117,7 +121,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
def single(self, movie, search_protocols = None, manual = False): def single(self, movie, search_protocols = None, manual = False):
# movies don't contain 'type' yet, so just set to default here # movies don't contain 'type' yet, so just set to default here
if not movie.has_key('type'): if 'type' not in movie:
movie['type'] = 'movie' movie['type'] = 'movie'
# Find out search type # Find out search type
@@ -133,8 +137,6 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.') log.debug('Movie doesn\'t have a profile or already done, assuming in manage tab.')
return return
db = get_session()
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True) release_dates = fireEvent('library.update.movie.release_date', identifier = movie['library']['identifier'], merge = True)
available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True) available_status, ignored_status, failed_status = fireEvent('status.get', ['available', 'ignored', 'failed'], single = True)
@@ -145,11 +147,12 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
default_title = getTitle(movie['library']) default_title = getTitle(movie['library'])
if not default_title: if not default_title:
log.error('No proper info found for movie, removing it from library to cause it from having more issues.') log.error('No proper info found for movie, removing it from library to cause it from having more issues.')
fireEvent('movie.delete', movie['id'], single = True) fireEvent('media.delete', movie['id'], single = True)
return return
fireEvent('notify.frontend', type = 'movie.searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title) fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, message = 'Searching for "%s"' % default_title)
db = get_session()
ret = False ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
@@ -192,7 +195,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
else: else:
log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title)) log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id']) fireEvent('media.restatus', movie['id'])
break break
# Break if CP wants to shut down # Break if CP wants to shut down
@@ -202,7 +205,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
if len(too_early_to_search) > 0: if len(too_early_to_search) > 0:
log.info2('Too early to search for %s, %s', (too_early_to_search, default_title)) log.info2('Too early to search for %s, %s', (too_early_to_search, default_title))
fireEvent('notify.frontend', type = 'movie.searcher.ended.%s' % movie['id'], data = True) fireEvent('notify.frontend', type = 'movie.searcher.ended', data = {'id': movie['id']})
return ret return ret
@@ -279,11 +282,17 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
now = int(time.time()) now = int(time.time())
now_year = date.today().year now_year = date.today().year
now_month = date.today().month
if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)): if (year is None or year < now_year - 1) and (not dates or (dates.get('theater', 0) == 0 and dates.get('dvd', 0) == 0)):
return True return True
else: else:
# Don't allow movies with years to far in the future
add_year = 1 if now_month > 10 else 0 # Only allow +1 year if end of the year
if year is not None and year > (now_year + add_year):
return False
# For movies before 1972 # For movies before 1972
if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0: if not dates or dates.get('theater', 0) < 0 or dates.get('dvd', 0) < 0:
return True return True
@@ -318,14 +327,14 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
'success': trynext 'success': trynext
} }
def tryNextRelease(self, movie_id, manual = False): def tryNextRelease(self, media_id, manual = False):
snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True) snatched_status, done_status, ignored_status = fireEvent('status.get', ['snatched', 'done', 'ignored'], single = True)
try: try:
db = get_session() db = get_session()
rels = db.query(Release) \ rels = db.query(Release) \
.filter_by(movie_id = movie_id) \ .filter_by(movie_id = media_id) \
.filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \ .filter(Release.status_id.in_([snatched_status.get('id'), done_status.get('id')])) \
.all() .all()
@@ -333,7 +342,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
rel.status_id = ignored_status.get('id') rel.status_id = ignored_status.get('id')
db.commit() db.commit()
movie_dict = fireEvent('movie.get', movie_id, single = True) movie_dict = fireEvent('media.get', media_id = media_id, single = True)
log.info('Trying next release for: %s', getTitle(movie_dict['library'])) log.info('Trying next release for: %s', getTitle(movie_dict['library']))
fireEvent('movie.searcher.single', movie_dict, manual = manual) fireEvent('movie.searcher.single', movie_dict, manual = manual)
@@ -341,7 +350,10 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
except: except:
log.error('Failed searching for next release: %s', traceback.format_exc()) log.error('Failed searching for next release: %s', traceback.format_exc())
db.rollback()
return False return False
finally:
db.close()
def getSearchTitle(self, media): def getSearchTitle(self, media):
if media['type'] == 'movie': if media['type'] == 'movie':
@@ -1,5 +1,6 @@
from .main import Suggestion from .main import Suggestion
def start(): def start():
return Suggestion() return Suggestion()
@@ -1,7 +1,7 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import splitString from couchpotato.core.helpers.variable import splitString, removeDuplicate
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Media, Library from couchpotato.core.settings.model import Media, Library
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -40,7 +40,7 @@ class Suggestion(Plugin):
movies.extend(splitString(Env.prop('suggest_seen', default = ''))) movies.extend(splitString(Env.prop('suggest_seen', default = '')))
suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True) suggestions = fireEvent('movie.suggest', movies = movies, ignore = ignored, single = True)
self.setCache('suggestion_cached', suggestions, timeout = 6048000) # Cache for 10 weeks self.setCache('suggestion_cached', suggestions, timeout = 6048000) # Cache for 10 weeks
return { return {
'success': True, 'success': True,
@@ -79,8 +79,10 @@ class Suggestion(Plugin):
seen = [] if not seen else seen seen = [] if not seen else seen
if ignore_imdb: if ignore_imdb:
suggested_imdbs = []
for cs in cached_suggestion: for cs in cached_suggestion:
if cs.get('imdb') != ignore_imdb: if cs.get('imdb') != ignore_imdb and cs.get('imdb') not in suggested_imdbs:
suggested_imdbs.append(cs.get('imdb'))
new_suggestions.append(cs) new_suggestions.append(cs)
# Get new results and add them # Get new results and add them
@@ -97,7 +99,7 @@ class Suggestion(Plugin):
movies.extend(seen) movies.extend(seen)
ignored.extend([x.get('imdb') for x in cached_suggestion]) ignored.extend([x.get('imdb') for x in cached_suggestion])
suggestions = fireEvent('movie.suggest', movies = movies, ignore = list(set(ignored)), single = True) suggestions = fireEvent('movie.suggest', movies = movies, ignore = removeDuplicate(ignored), single = True)
if suggestions: if suggestions:
new_suggestions.extend(suggestions) new_suggestions.extend(suggestions)
@@ -101,7 +101,7 @@ var SuggestList = new Class({
// Add rating // Add rating
m.info_container.adopt( m.info_container.adopt(
m.rating = m.info.rating && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', { m.rating = m.info.rating && m.info.rating.imdb && m.info.rating.imdb.length == 2 && parseFloat(m.info.rating.imdb[0]) > 0 ? new Element('span.rating', {
'text': parseFloat(m.info.rating.imdb[0]), 'text': parseFloat(m.info.rating.imdb[0]),
'title': parseInt(m.info.rating.imdb[1]) + ' votes' 'title': parseInt(m.info.rating.imdb[1]) + ' votes'
}) : null, }) : null,
@@ -13,5 +13,6 @@ def upgrade(migrate_engine):
create_column(category_column, movie) create_column(category_column, movie)
Index('ix_movie_category_id', movie.c.category_id).create() Index('ix_movie_category_id', movie.c.category_id).create()
def downgrade(migrate_engine): def downgrade(migrate_engine):
pass pass
+1 -1
View File
@@ -17,7 +17,7 @@ class Notification(Provider):
listen_to = [ listen_to = [
'renamer.after', 'movie.snatched', 'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
'core.message', 'core.message.important',
] ]
dont_listen_to = [] dont_listen_to = []
@@ -1,5 +1,6 @@
from .main import Boxcar from .main import Boxcar
def start(): def start():
return Boxcar() return Boxcar()
@@ -16,14 +16,14 @@ class Boxcar(Notification):
try: try:
message = message.strip() message = message.strip()
params = { data = {
'email': self.conf('email'), 'email': self.conf('email'),
'notification[from_screen_name]': self.default_title, 'notification[from_screen_name]': self.default_title,
'notification[message]': toUnicode(message), 'notification[message]': toUnicode(message),
'notification[from_remote_service_id]': int(time.time()), 'notification[from_remote_service_id]': int(time.time()),
} }
self.urlopen(self.url, params = params) self.urlopen(self.url, data = data)
except: except:
log.error('Check your email and added services on boxcar.io') log.error('Check your email and added services on boxcar.io')
return False return False
@@ -0,0 +1,34 @@
from .main import Boxcar2
def start():
return Boxcar2()
config = [{
'name': 'boxcar2',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'boxcar2',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'token',
'description': ('Your Boxcar access token.', 'Can be found in the app under settings')
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -0,0 +1,39 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
class Boxcar2(Notification):
url = 'https://new.boxcar.io/api/notifications'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
try:
message = message.strip()
long_message = ''
if listener == 'test':
long_message = 'This is a test message'
elif data.get('identifier'):
long_message = 'More movie info <a href="http://www.imdb.com/title/%s/">on IMDB</a>' % data['identifier']
data = {
'user_credentials': self.conf('token'),
'notification[title]': toUnicode(message),
'notification[long_message]': toUnicode(long_message),
}
self.urlopen(self.url, data = data)
except:
log.error('Make sure the token provided is for the correct device')
return False
log.info('Boxcar notification successful.')
return True
def isEnabled(self):
return super(Boxcar2, self).isEnabled() and self.conf('token')
@@ -1,5 +1,6 @@
from .main import CoreNotifier from .main import CoreNotifier
def start(): def start():
return CoreNotifier() return CoreNotifier()
+55 -26
View File
@@ -21,6 +21,12 @@ class CoreNotifier(Notification):
m_lock = None m_lock = None
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message', 'core.message.important',
]
def __init__(self): def __init__(self):
super(CoreNotifier, self).__init__() super(CoreNotifier, self).__init__()
@@ -61,28 +67,42 @@ class CoreNotifier(Notification):
def clean(self): def clean(self):
db = get_session() try:
db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete() db = get_session()
db.commit() db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
db.commit()
except:
log.error('Failed cleaning notification: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def markAsRead(self, ids = None, **kwargs): def markAsRead(self, ids = None, **kwargs):
ids = splitString(ids) if ids else None ids = splitString(ids) if ids else None
db = get_session() try:
db = get_session()
if ids: if ids:
q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids])) q = db.query(Notif).filter(or_(*[Notif.id == tryInt(s) for s in ids]))
else: else:
q = db.query(Notif).filter_by(read = False) q = db.query(Notif).filter_by(read = False)
q.update({Notif.read: True}) q.update({Notif.read: True})
db.commit()
db.commit() return {
'success': True
}
except:
log.error('Failed mark as read: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return { return {
'success': True 'success': False
} }
def listView(self, limit_offset = None, **kwargs): def listView(self, limit_offset = None, **kwargs):
@@ -121,7 +141,10 @@ class CoreNotifier(Notification):
for message in messages: for message in messages:
if message.get('time') > last_check: if message.get('time') > last_check:
fireEvent('core.message', message = message.get('message'), data = message) message['sticky'] = True # Always sticky core messages
message_type = 'core.message.important' if message.get('important') else 'core.message'
fireEvent(message_type, message = message.get('message'), data = message)
if last_check < message.get('time'): if last_check < message.get('time'):
last_check = message.get('time') last_check = message.get('time')
@@ -131,24 +154,30 @@ class CoreNotifier(Notification):
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
db = get_session() try:
db = get_session()
data['notification_type'] = listener if listener else 'unknown' data['notification_type'] = listener if listener else 'unknown'
n = Notif( n = Notif(
message = toUnicode(message), message = toUnicode(message),
data = data data = data
) )
db.add(n) db.add(n)
db.commit() db.commit()
ndict = n.to_dict() ndict = n.to_dict()
ndict['type'] = 'notification' ndict['type'] = 'notification'
ndict['time'] = time.time() ndict['time'] = time.time()
self.frontend(type = listener, data = data) self.frontend(type = listener, data = data)
return True return True
except:
log.error('Failed notify: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
def frontend(self, type = 'notification', data = None, message = None): def frontend(self, type = 'notification', data = None, message = None):
if not data: data = {} if not data: data = {}
@@ -10,8 +10,8 @@ var NotificationBase = new Class({
// Listener // Listener
App.addEvent('unload', self.stopPoll.bind(self)); App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('reload', self.startInterval.bind(self, [true])); App.addEvent('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self)); App.on('notification', self.notify.bind(self));
App.addEvent('message', self.showMessage.bind(self)); App.on('message', self.showMessage.bind(self));
// Add test buttons to settings page // Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self)); App.addEvent('load', self.addTestButtons.bind(self));
@@ -50,9 +50,9 @@ var NotificationBase = new Class({
, 'top'); , 'top');
self.notifications.include(result); self.notifications.include(result);
if(result.data.important !== undefined && !result.read){ if((result.data.important !== undefined || result.data.sticky !== undefined) && !result.read){
var sticky = true var sticky = true
App.fireEvent('message', [result.message, sticky, result]) App.trigger('message', [result.message, sticky, result])
} }
else if(!result.read){ else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
@@ -147,7 +147,7 @@ var NotificationBase = new Class({
// Process data // Process data
if(json){ if(json){
Array.each(json.result, function(result){ Array.each(json.result, function(result){
App.fireEvent(result.type, result); App.trigger(result.type, [result]);
if(result.message && result.read === undefined) if(result.message && result.read === undefined)
self.showMessage(result.message); self.showMessage(result.message);
}) })
@@ -1,5 +1,6 @@
from .main import Email from .main import Email
def start(): def start():
return Email() return Email()
@@ -30,7 +31,7 @@ config = [{
}, },
{ 'name': 'smtp_port', { 'name': 'smtp_port',
'label': 'SMTP server port', 'label': 'SMTP server port',
'default': '25', 'default': '25',
'type': 'int', 'type': 'int',
}, },
{ {
+4 -1
View File
@@ -4,6 +4,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.environment import Env from couchpotato.environment import Env
from email.mime.text import MIMEText from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
import smtplib import smtplib
import traceback import traceback
@@ -30,6 +31,8 @@ class Email(Notification):
message['Subject'] = self.default_title message['Subject'] = self.default_title
message['From'] = from_address message['From'] = from_address
message['To'] = to_address message['To'] = to_address
message['Date'] = formatdate(localtime = 1)
message['Message-ID'] = make_msgid()
try: try:
# Open the SMTP connection, via SSL if requested # Open the SMTP connection, via SSL if requested
@@ -37,7 +40,7 @@ class Email(Notification):
log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled")) log.debug("SMTP over SSL %s", ("enabled" if ssl == 1 else "disabled"))
mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server) mailserver = smtplib.SMTP_SSL(smtp_server) if ssl == 1 else smtplib.SMTP(smtp_server)
if (starttls): if starttls:
log.debug("Using StartTLS to initiate the connection with the SMTP server") log.debug("Using StartTLS to initiate the connection with the SMTP server")
mailserver.starttls() mailserver.starttls()
@@ -1,5 +1,6 @@
from .main import Growl from .main import Growl
def start(): def start():
return Growl() return Growl()
+1 -1
View File
@@ -37,7 +37,7 @@ class Growl(Notification):
) )
self.growl.register() self.growl.register()
self.registered = True self.registered = True
except Exception, e: except Exception as e:
if 'timed out' in str(e): if 'timed out' in str(e):
self.registered = True self.registered = True
else: else:
@@ -1,5 +1,6 @@
from .main import NMJ from .main import NMJ
def start(): def start():
return NMJ() return NMJ()
+3 -4
View File
@@ -86,18 +86,17 @@ class NMJ(Notification):
'arg3': '', 'arg3': '',
} }
params = tryUrlencode(params) params = tryUrlencode(params)
UPDATE_URL = 'http://%(host)s:8008/metadata_database?%(params)s' update_url = 'http://%(host)s:8008/metadata_database?%(params)s' % {'host': host, 'params': params}
updateUrl = UPDATE_URL % {'host': host, 'params': params}
try: try:
response = self.urlopen(updateUrl) response = self.urlopen(update_url)
except: except:
return False return False
try: try:
et = etree.fromstring(response) et = etree.fromstring(response)
result = et.findtext('returnValue') result = et.findtext('returnValue')
except SyntaxError, e: except SyntaxError as e:
log.error('Unable to parse XML returned from the Popcorn Hour: %s', e) log.error('Unable to parse XML returned from the Popcorn Hour: %s', e)
return False return False
@@ -1,5 +1,6 @@
from .main import NotifyMyAndroid from .main import NotifyMyAndroid
def start(): def start():
return NotifyMyAndroid() return NotifyMyAndroid()
@@ -2,6 +2,7 @@ from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
import pynma import pynma
import six
log = CPLog(__name__) log = CPLog(__name__)
@@ -26,7 +27,7 @@ class NotifyMyAndroid(Notification):
successful = 0 successful = 0
for key in keys: for key in keys:
if not response[str(key)]['code'] == u'200': if not response[str(key)]['code'] == six.u('200'):
log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message'])) log.error('Could not send notification to NotifyMyAndroid (%s). %s', (key, response[key]['message']))
else: else:
successful += 1 successful += 1
@@ -1,5 +1,6 @@
from .main import NotifyMyWP from .main import NotifyMyWP
def start(): def start():
return NotifyMyWP() return NotifyMyWP()
@@ -2,13 +2,15 @@ from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from pynmwp import PyNMWP from pynmwp import PyNMWP
import six
log = CPLog(__name__) log = CPLog(__name__)
class NotifyMyWP(Notification): class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
keys = splitString(self.conf('api_key')) keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key')) p = PyNMWP(keys, self.conf('dev_key'))
@@ -16,7 +18,7 @@ class NotifyMyWP(Notification):
response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1) response = p.push(application = self.default_title, event = message, description = message, priority = self.conf('priority'), batch_mode = len(keys) > 1)
for key in keys: for key in keys:
if not response[key]['Code'] == u'200': if not response[key]['Code'] == six.u('200'):
log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message'])) log.error('Could not send notification to NotifyMyWindowsPhone (%s). %s', (key, response[key]['message']))
return False return False
@@ -1,5 +1,6 @@
from .main import Plex from .main import Plex
def start(): def start():
return Plex() return Plex()
@@ -29,7 +29,7 @@ class PlexClientHTTP(PlexClientProtocol):
try: try:
self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False) self.plex.urlopen(url, headers = headers, timeout = 3, show_error = False)
except Exception, err: except Exception as err:
log.error("Couldn't sent command to Plex: %s", err) log.error("Couldn't sent command to Plex: %s", err)
return False return False
@@ -68,7 +68,7 @@ class PlexClientJSON(PlexClientProtocol):
try: try:
requests.post(url, headers = headers, timeout = 3, data = json.dumps(request)) requests.post(url, headers = headers, timeout = 3, data = json.dumps(request))
except Exception, err: except Exception as err:
log.error("Couldn't sent command to Plex: %s", err) log.error("Couldn't sent command to Plex: %s", err)
return False return False
+4 -3
View File
@@ -23,9 +23,9 @@ class Plex(Notification):
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, message = None, group = None):
def addToLibrary(self, message = None, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
if not group: group = {}
return self.server.refresh() return self.server.refresh()
@@ -57,7 +57,8 @@ class Plex(Notification):
return success return success
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {}
return self.notifyClients(message, self.getClientNames()) return self.notifyClients(message, self.getClientNames())
def test(self, **kwargs): def test(self, **kwargs):
@@ -1,5 +1,6 @@
from .main import Prowl from .main import Prowl
def start(): def start():
return Prowl() return Prowl()
+2 -2
View File
@@ -22,11 +22,11 @@ class Prowl(Notification):
'priority': self.conf('priority'), 'priority': self.conf('priority'),
} }
headers = { headers = {
'Content-type': 'application/x-www-form-urlencoded' 'Content-type': 'application/x-www-form-urlencoded'
} }
try: try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False) self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
log.info('Prowl notifications sent.') log.info('Prowl notifications sent.')
return True return True
except: except:
@@ -1,5 +1,6 @@
from .main import Pushalot from .main import Pushalot
def start(): def start():
return Pushalot() return Pushalot()
@@ -5,6 +5,7 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Pushalot(Notification): class Pushalot(Notification):
urls = { urls = {
@@ -29,7 +30,7 @@ class Pushalot(Notification):
} }
try: try:
self.urlopen(self.urls['api'], headers = headers, params = data, multipart = True, show_error = False) self.urlopen(self.urls['api'], headers = headers, data = data, show_error = False)
return True return True
except: except:
log.error('PushAlot failed: %s', traceback.format_exc()) log.error('PushAlot failed: %s', traceback.format_exc())
@@ -0,0 +1,40 @@
from .main import Pushbullet
def start():
return Pushbullet()
config = [{
'name': 'pushbullet',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'pushbullet',
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'api_key',
'label': 'User API Key'
},
{
'name': 'devices',
'default': '',
'advanced': True,
'description': 'IDs of devices to send notifications to, empty = all devices'
},
{
'name': 'on_snatch',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Also send message when movie is snatched.',
},
],
}
],
}]
@@ -0,0 +1,69 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
import base64
import json
log = CPLog(__name__)
class Pushbullet(Notification):
url = 'https://api.pushbullet.com/api/%s'
def notify(self, message = '', data = None, listener = None):
if not data: data = {}
devices = self.getDevices()
if devices is None:
return False
# Get all the device IDs linked to this user
if not len(devices):
response = self.request('devices')
if not response:
return False
devices += [device.get('id') for device in response['devices']]
successful = 0
for device in devices:
response = self.request(
'pushes',
cache = False,
device_iden = device,
type = 'note',
title = self.default_title,
body = toUnicode(message)
)
if response:
successful += 1
else:
log.error('Unable to push notification to Pushbullet device with ID %s' % device)
return successful == len(devices)
def getDevices(self):
return splitString(self.conf('devices'))
def request(self, method, cache = True, **kwargs):
try:
base64string = base64.encodestring('%s:' % self.conf('api_key'))[:-1]
headers = {
"Authorization": "Basic %s" % base64string
}
if cache:
return self.getJsonData(self.url % method, headers = headers, data = kwargs)
else:
data = self.urlopen(self.url % method, headers = headers, data = kwargs)
return json.loads(data)
except Exception as ex:
log.error('Pushbullet request failed')
log.debug(ex)
return None
@@ -1,5 +1,6 @@
from .main import Pushover from .main import Pushover
def start(): def start():
return Pushover() return Pushover()
@@ -30,9 +30,9 @@ class Pushover(Notification):
}) })
http_handler.request('POST', http_handler.request('POST',
"/1/messages.json", "/1/messages.json",
headers = {'Content-type': 'application/x-www-form-urlencoded'}, headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(api_data) body = tryUrlencode(api_data)
) )
response = http_handler.getresponse() response = http_handler.getresponse()
@@ -1,5 +1,6 @@
from .main import Synoindex from .main import Synoindex
def start(): def start():
return Synoindex() return Synoindex()
@@ -26,7 +26,7 @@ class Synoindex(Notification):
out = p.communicate() out = p.communicate()
log.info('Result from synoindex: %s', str(out)) log.info('Result from synoindex: %s', str(out))
return True return True
except OSError, e: except OSError as e:
log.error('Unable to run synoindex: %s', e) log.error('Unable to run synoindex: %s', e)
return False return False
@@ -1,5 +1,6 @@
from .main import Toasty from .main import Toasty
def start(): def start():
return Toasty() return Toasty()
@@ -5,6 +5,7 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Toasty(Notification): class Toasty(Notification):
urls = { urls = {
@@ -1,5 +1,6 @@
from .main import Trakt from .main import Trakt
def start(): def start():
return Trakt() return Trakt()
+30 -14
View File
@@ -3,12 +3,14 @@ from couchpotato.core.notifications.base import Notification
log = CPLog(__name__) log = CPLog(__name__)
class Trakt(Notification): class Trakt(Notification):
urls = { urls = {
'base': 'http://api.trakt.tv/%s', 'base': 'http://api.trakt.tv/%s',
'library': 'movie/library/%s', 'library': 'movie/library/%s',
'unwatchlist': 'movie/unwatchlist/%s', 'unwatchlist': 'movie/unwatchlist/%s',
'test': 'account/test/%s',
} }
listen_to = ['movie.downloaded'] listen_to = ['movie.downloaded']
@@ -16,26 +18,40 @@ class Trakt(Notification):
def notify(self, message = '', data = None, listener = None): def notify(self, message = '', data = None, listener = None):
if not data: data = {} if not data: data = {}
post_data = { if listener == 'test':
'username': self.conf('automation_username'),
'password' : self.conf('automation_password'),
'movies': [{
'imdb_id': data['library']['identifier'],
'title': data['library']['titles'][0]['title'],
'year': data['library']['year']
}] if data else []
}
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data) post_data = {
if self.conf('remove_watchlist_enabled'): 'username': self.conf('automation_username'),
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data) 'password': self.conf('automation_password'),
}
return result result = self.call((self.urls['test'] % self.conf('automation_api_key')), post_data)
return result
else:
post_data = {
'username': self.conf('automation_username'),
'password': self.conf('automation_password'),
'movies': [{
'imdb_id': data['library']['identifier'],
'title': data['library']['titles'][0]['title'],
'year': data['library']['year']
}] if data else []
}
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
if self.conf('remove_watchlist_enabled'):
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
return result
def call(self, method_url, post_data): def call(self, method_url, post_data):
try: try:
response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
response = self.getJsonData(self.urls['base'] % method_url, data = post_data, cache_timeout = 1)
if response: if response:
if response.get('status') == "success": if response.get('status') == "success":
log.info('Successfully called Trakt') log.info('Successfully called Trakt')
@@ -1,5 +1,6 @@
from .main import Twitter from .main import Twitter
def start(): def start():
return Twitter() return Twitter()
@@ -64,7 +64,7 @@ class Twitter(Notification):
api.PostUpdate(update_message[135:] + ' 2/2') api.PostUpdate(update_message[135:] + ' 2/2')
else: else:
api.PostUpdate(update_message) api.PostUpdate(update_message)
except Exception, e: except Exception as e:
log.error('Error sending tweet: %s', e) log.error('Error sending tweet: %s', e)
return False return False
Loaded 100 of 470 files, more files were not shown because too many files have changed in this diff. Show more