Merge branch 'refs/heads/develop'

This commit is contained in:
Ruud committed 2014-02-14 19:39:47 +01:00
commit d6921882e1
255 files changed
+2191 -1306

No files matched your search

+5 -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,14 @@ 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.
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 +149,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 -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()
+1 -1
View File
@@ -117,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()
+3 -3
View File
@@ -53,9 +53,9 @@ class ClientScript(Plugin):
} }
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()
@@ -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()
+15 -14
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__)
@@ -63,7 +64,7 @@ class Updater(Plugin):
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:
@@ -151,6 +152,9 @@ class BaseUpdater(Plugin):
'branch': self.branch, 'branch': self.branch,
} }
def getVersion(self):
pass
def check(self): def check(self):
pass pass
@@ -179,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):
@@ -206,7 +209,7 @@ 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())), 'repr': 'git:(%s:%s % s) %s (%s)' % (self.repo_user, self.repo_name, self.branch, output.hash[:8], datetime.fromtimestamp(output.getDate())),
@@ -214,7 +217,7 @@ class GitUpdater(BaseUpdater):
'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'
@@ -250,7 +253,6 @@ class GitUpdater(BaseUpdater):
return False return False
class SourceUpdater(BaseUpdater): class SourceUpdater(BaseUpdater):
def __init__(self): def __init__(self):
@@ -276,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)
@@ -345,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):
@@ -366,7 +367,7 @@ class SourceUpdater(BaseUpdater):
self.version = output self.version = output
self.version['type'] = 'source' self.version['type'] = 'source'
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))) 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, e: except Exception as e:
log.error('Failed using source updater. %s', e) log.error('Failed using source updater. %s', e)
return {} return {}
@@ -396,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())
@@ -441,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,
} }
@@ -24,7 +24,7 @@ var UpdaterBase = new Class({
self.doUpdate(); self.doUpdate();
else { else {
App.unBlockPage(); App.unBlockPage();
App.on('message', 'No updates available'); App.trigger('message', ['No updates available']);
} }
} }
}) })
@@ -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()
@@ -1,5 +1,6 @@
from .main import Deluge from .main import Deluge
def start(): def start():
return Deluge() return Deluge()
+10 -9
View File
@@ -109,7 +109,7 @@ class Deluge(Downloader):
continue 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'])) 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'
status = 'busy' status = 'busy'
@@ -125,11 +125,11 @@ class Deluge(Downloader):
download_dir = sp(torrent['save_path']) download_dir = sp(torrent['save_path'])
if torrent['move_on_completed']: if torrent['move_on_completed']:
download_dir = torrent['move_completed_path'] download_dir = torrent['move_completed_path']
torrent_files = [] torrent_files = []
for file_item in torrent['files']: for file_item in torrent['files']:
torrent_files.append(sp(os.path.join(download_dir, file_item['path']))) torrent_files.append(sp(os.path.join(download_dir, file_item['path'])))
release_downloads.append({ release_downloads.append({
'id': torrent['hash'], 'id': torrent['hash'],
'name': torrent['name'], 'name': torrent['name'],
@@ -157,6 +157,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'
@@ -187,7 +188,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:
@@ -205,7 +206,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:
@@ -218,7 +219,7 @@ class DelugeRPC(object):
try: try:
self.connect() self.connect()
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() 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:
@@ -229,7 +230,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:
@@ -239,7 +240,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:
@@ -250,7 +251,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()
+5 -5
View File
@@ -42,7 +42,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:
@@ -56,7 +56,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]
@@ -83,7 +83,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 [] 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:
@@ -116,7 +116,7 @@ class NZBGet(Downloader):
timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) timeleft = str(timedelta(seconds = nzb['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20))
except: except:
pass pass
release_downloads.append({ release_downloads.append({
'id': nzb_id, 'id': nzb_id,
'name': nzb['NZBFilename'], 'name': nzb['NZBFilename'],
@@ -169,7 +169,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:
@@ -1,5 +1,6 @@
from .main import NZBVortex from .main import NZBVortex
def start(): def start():
return NZBVortex() return NZBVortex()
@@ -56,13 +56,13 @@ class NZBVortex(Downloader):
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']),
}) })
@@ -102,7 +102,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
@@ -123,7 +122,7 @@ class NZBVortex(Downloader):
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:
@@ -145,7 +144,7 @@ class NZBVortex(Downloader):
try: try:
data = self.urlopen(url, 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:
@@ -175,6 +174,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()
@@ -26,26 +26,26 @@ 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, media)) 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, media) 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 self.downloadReturnId('') return self.downloadReturnId('')
else: else:
log.info('File %s already exists.', fullPath) log.info('File %s already exists.', full_path)
return self.downloadReturnId('') return self.downloadReturnId('')
except: except:
@@ -1,5 +1,6 @@
from .main import rTorrent from .main import rTorrent
def start(): def start():
return rTorrent() return rTorrent()
+14 -8
View File
@@ -42,7 +42,7 @@ class rTorrent(Downloader):
if self.rt is not None: if self.rt is not None:
return self.rt return self.rt
url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + '/' + self.conf('rpc_url').strip('/ ') + '/' url = cleanHost(self.conf('host'), protocol = True, ssl = self.conf('ssl')) + 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(
@@ -87,7 +87,7 @@ 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
@@ -111,7 +111,6 @@ class rTorrent(Downloader):
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
@@ -135,7 +134,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?')
@@ -156,7 +155,7 @@ 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
@@ -173,9 +172,16 @@ class rTorrent(Downloader):
for torrent in torrents: for torrent in torrents:
if torrent.info_hash in ids: if torrent.info_hash in ids:
torrent_directory = os.path.normpath(torrent.directory)
torrent_files = [] torrent_files = []
for file_item in torrent.get_files():
torrent_files.append(sp(os.path.join(torrent.directory, file_item.path))) for file in torrent.get_files():
if not os.path.normpath(file.path).startswith(torrent_directory):
file_path = os.path.join(torrent_directory, file.path.lstrip('/'))
else:
file_path = file.path
torrent_files.append(sp(file_path))
status = 'busy' status = 'busy'
if torrent.complete: if torrent.complete:
@@ -197,7 +203,7 @@ class rTorrent(Downloader):
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 [] return []
@@ -1,5 +1,6 @@
from .main import Sabnzbd from .main import Sabnzbd
def start(): def start():
return Sabnzbd() return Sabnzbd()
+4 -4
View File
@@ -95,7 +95,7 @@ class Sabnzbd(Downloader):
status = 'busy' status = 'busy'
if 'ENCRYPTED / ' in nzb['filename']: if 'ENCRYPTED / ' in nzb['filename']:
status = 'failed' status = 'failed'
release_downloads.append({ release_downloads.append({
'id': nzb['nzo_id'], 'id': nzb['nzo_id'],
'name': nzb['filename'], 'name': nzb['filename'],
@@ -112,7 +112,7 @@ class Sabnzbd(Downloader):
status = 'failed' status = 'failed'
elif nzb['status'] == 'Completed': elif nzb['status'] == 'Completed':
status = 'completed' status = 'completed'
release_downloads.append({ release_downloads.append({
'id': nzb['nzo_id'], 'id': nzb['nzo_id'],
'name': nzb['name'], 'name': nzb['name'],
@@ -166,8 +166,8 @@ 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'), ssl = self.conf('ssl')) + '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()
@@ -65,6 +65,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"""
@@ -107,11 +108,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
@@ -1,5 +1,6 @@
from .main import Transmission from .main import Transmission
def start(): def start():
return Transmission() return Transmission()
@@ -105,7 +105,7 @@ class Transmission(Downloader):
for torrent in queue['torrents']: for torrent in queue['torrents']:
if torrent['hashString'] in ids: if torrent['hashString'] in ids:
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', 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['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']))
status = 'busy' status = 'busy'
if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'): if torrent.get('isStalled') and not torrent['percentDone'] == 1 and self.conf('stalled_as_failed'):
@@ -187,10 +187,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
@@ -208,7 +208,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',
@@ -66,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.')
@@ -149,7 +149,7 @@ class uTorrent(Downloader):
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 = 'busy' status = 'busy'
if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000: if (torrent[1] & self.status_flags['STARTED'] or torrent[1] & self.status_flags['QUEUED']) and torrent[4] == 1000:
status = 'seeding' status = 'seeding'
@@ -157,10 +157,10 @@ class uTorrent(Downloader):
status = 'failed' status = 'failed'
elif torrent[4] == 1000: elif torrent[4] == 1000:
status = 'completed' status = 'completed'
if not status == 'busy': if not status == 'busy':
self.removeReadOnly(torrent_files) self.removeReadOnly(torrent_files)
release_downloads.append({ release_downloads.append({
'id': torrent[0], 'id': torrent[0],
'name': torrent[2], 'name': torrent[2],
@@ -231,14 +231,14 @@ 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
@@ -261,7 +261,7 @@ class uTorrentAPI(object):
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)
@@ -304,7 +304,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
+20 -10
View File
@@ -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,24 @@ def addEvent(name, handler, priority = 100):
'priority': priority, 'priority': priority,
}) })
def removeEvent(name, handler): def removeEvent(name, handler):
e = events[name] e = events[name]
e -= handler 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 +105,14 @@ def fireEvent(name, *args, **kwargs):
# Fire # Fire
result = e(*args, **kwargs) result = e(*args, **kwargs)
result_keys = result.keys()
result_keys.sort(natcmp)
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 +124,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 +167,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]
+16 -7
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,16 +41,18 @@ 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 '\'
@@ -73,6 +78,7 @@ def sp(path, *args):
return path return path
def ek(original, *args): def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
try: try:
@@ -83,6 +89,7 @@ def ek(original, *args):
return original return original
def isInt(value): def isInt(value):
try: try:
int(value) int(value)
@@ -90,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:
+4 -3
View File
@@ -9,7 +9,7 @@ def getParams(params):
reg = re.compile('^[a-z0-9_\.]+$') reg = re.compile('^[a-z0-9_\.]+$')
temp = {} temp = {}
for param, value in sorted(params.iteritems()): for param, value in sorted(params.items()):
nest = re.split("([\[\]]+)", param) nest = re.split("([\[\]]+)", param)
if len(nest) > 1: if len(nest) > 1:
@@ -37,13 +37,14 @@ 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)] new_value = [dictToList(value[k]) for k in sorted(value.keys(), cmp = natcmp)]
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 []
+77 -15
View File
@@ -8,26 +8,32 @@ 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): def fnEscape(pattern):
return pattern.replace('[','[[').replace(']','[]]').replace('[[','[[]') 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
@@ -37,6 +43,7 @@ def getUserDir():
return os.path.expanduser('~') return os.path.expanduser('~')
def getDownloadDir(): def getDownloadDir():
user_dir = getUserDir() user_dir = getUserDir()
@@ -49,6 +56,7 @@ def getDownloadDir():
return user_dir return user_dir
def getDataDir(): def getDataDir():
# Windows # Windows
@@ -68,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)
@@ -91,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:
@@ -98,38 +109,65 @@ 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, protocol = True, ssl = False, username = None, password = None): def cleanHost(host, protocol = True, ssl = False, username = None, password = None):
"""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: if not '://' in host and protocol:
host = 'https://' if ssl else 'http://' + host host = ('https://' if ssl else 'http://') + host
if not protocol: if not protocol:
host = host.split('://', 1)[-1] host = host.split('://', 1)[-1]
if protocol and username and password: if protocol and username and password:
login = '%s:%s@' % (username, password) try:
if not login in host: auth = re.findall('^(?:.+?//)(.+?):(.+?)@(?:.+)$', host)
host = host.replace('://', '://' + login, 1) 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('/ ') host = host.rstrip('/ ')
if protocol: if protocol:
@@ -137,6 +175,7 @@ def cleanHost(host, protocol = True, ssl = False, username = None, password = No
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:
@@ -153,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:
@@ -161,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):
@@ -173,17 +214,24 @@ def tryFloat(s):
return float(s) return float(s)
except: return 0 except: return 0
def natsortKey(s): def natsortKey(s):
return map(tryInt, re.findall(r'(\d+|\D+)', s)) return map(tryInt, re.findall(r'(\d+|\D+)', s))
def natcmp(a, b): def natcmp(a, b):
return cmp(natsortKey(a), natsortKey(b)) a2 = natsortKey(a)
b2 = natsortKey(b)
return (a2 > b2) - (a2 < b2)
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:
@@ -206,6 +254,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 = [
@@ -218,18 +267,31 @@ 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): def isSubFolder(sub_folder, base_folder):
# Returns True if sub_folder is the same as or inside base_folder # Returns True if sub_folder is the same as or inside base_folder
return base_folder and sub_folder and os.path.normpath(base_folder).rstrip(os.path.sep) + os.path.sep in os.path.normpath(sub_folder).rstrip(os.path.sep) + os.path.sep 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)
+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)
+26 -15
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,25 +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()
media_dict = media.to_dict(self.default_dict) media = db.query(Media).filter_by(id = id).first()
event_name = '%s.searcher.single' % media.type media_dict = media.to_dict(self.default_dict)
db.expire_all() event_name = '%s.searcher.single' % media.type
fireEvent(event_name, media_dict, on_complete = self.createNotifyFront(id)) 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()
media_dict = media.to_dict(self.default_dict) media = db.query(Media).filter_by(id = media_id).first()
event_name = '%s.update' % media.type media_dict = media.to_dict(self.default_dict)
db.expire_all() event_name = '%s.update' % media.type
fireEvent('notify.frontend', type = event_name, data = media_dict) 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()
+70 -63
View File
@@ -1,3 +1,4 @@
import traceback
from couchpotato import get_session, tryInt 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
@@ -70,8 +71,6 @@ class MediaPlugin(MediaBase):
addEvent('media.restatus', self.restatus) addEvent('media.restatus', self.restatus)
def refresh(self, id = '', **kwargs): def refresh(self, id = '', **kwargs):
db = get_session()
handlers = [] handlers = []
ids = splitString(id) ids = splitString(id)
@@ -97,12 +96,12 @@ class MediaPlugin(MediaBase):
default_title = getTitle(media.library) default_title = getTitle(media.library)
identifier = media.library.identifier identifier = media.library.identifier
db.expire_all() event = 'library.update.%s' % media.type
def handler(): def handler():
fireEvent('library.update.%s' % media.type, identifier = identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id)) fireEvent(event, identifier = identifier, default_title = default_title, on_complete = self.createOnComplete(id))
if handler:
return handler return handler
def addSingleRefreshView(self): def addSingleRefreshView(self):
@@ -125,7 +124,6 @@ class MediaPlugin(MediaBase):
if m: if m:
results = m.to_dict(self.default_dict) results = m.to_dict(self.default_dict)
db.expire_all()
return results return results
def getView(self, id = None, **kwargs): def getView(self, id = None, **kwargs):
@@ -254,14 +252,13 @@ class MediaPlugin(MediaBase):
# Merge releases with movie dict # Merge releases with movie dict
movies.append(mergeDicts(movie_dict[media_id].to_dict({ movies.append(mergeDicts(movie_dict[media_id].to_dict({
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files': {}},
'files': {}, 'files': {},
}), { }), {
'releases': releases, 'releases': releases,
'releases_count': releases_count.get(media_id), 'releases_count': releases_count.get(media_id),
})) }))
db.expire_all()
return total_count, movies return total_count, movies
def listView(self, **kwargs): def listView(self, **kwargs):
@@ -355,7 +352,6 @@ class MediaPlugin(MediaBase):
if len(chars) == 25: if len(chars) == 25:
break break
db.expire_all()
return ''.join(sorted(chars)) return ''.join(sorted(chars))
def charView(self, **kwargs): def charView(self, **kwargs):
@@ -380,50 +376,55 @@ class MediaPlugin(MediaBase):
def delete(self, media_id, delete_from = None): def delete(self, media_id, delete_from = None):
db = get_session() try:
db = get_session()
media = db.query(Media).filter_by(id = media_id).first() media = db.query(Media).filter_by(id = media_id).first()
if media: if media:
deleted = False deleted = False
if delete_from == 'all': 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.delete(media)
db.commit() db.commit()
deleted = True 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: else:
fireEvent('media.restatus', media.id, single = True) done_status = fireEvent('status.get', 'done', single = True)
if deleted: total_releases = len(media.releases)
fireEvent('notify.frontend', type = 'movie.deleted', data = media.to_dict()) 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()
db.expire_all()
return True return True
def deleteView(self, id = '', **kwargs): def deleteView(self, id = '', **kwargs):
@@ -447,27 +448,33 @@ class MediaPlugin(MediaBase):
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True) active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
db = get_session() try:
db = get_session()
m = db.query(Media).filter_by(id = media_id).first() m = db.query(Media).filter_by(id = media_id).first()
if not m or len(m.library.titles) == 0: if not m or len(m.library.titles) == 0:
log.debug('Can\'t restatus movie, doesn\'t seem to exist.') log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
return False return False
log.debug('Changing status for %s', m.library.titles[0].title) log.debug('Changing status for %s', m.library.titles[0].title)
if not m.profile: if not m.profile:
m.status_id = done_status.get('id') m.status_id = done_status.get('id')
else: else:
move_to_wanted = True move_to_wanted = True
for t in m.profile.types: for t in m.profile.types:
for release in m.releases: 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): if t.quality.identifier is release.quality.identifier and (release.status_id is done_status.get('id') and t.finish):
move_to_wanted = False move_to_wanted = False
m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id') m.status_id = active_status.get('id') if move_to_wanted else done_status.get('id')
db.commit() db.commit()
return True 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()
+105 -91
View File
@@ -1,3 +1,4 @@
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
@@ -61,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
@@ -71,76 +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
if params.get('title'): for release in m.releases:
message = 'Successfully added "%s" to your wanted list.' % params.get('title', '') 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: else:
title = getTitle(m.library) log.debug('Movie already exists, not updating: %s', params)
if title: added = False
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)
db.expire_all() if force_readd:
return movie_dict 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)
@@ -152,42 +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 media_id in ids: for media_id in ids:
m = db.query(Media).filter_by(id = media_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() db.commit()
fireEvent('media.restatus', m.id) fireEvent('media.restatus', m.id)
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id)) fireEventAsync('movie.searcher.single', movie_dict, on_complete = self.createNotifyFront(media_id))
return {
'success': True,
}
except:
log.error('Failed deleting media: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
db.expire_all()
return { return {
'success': True, 'success': False,
} }
@@ -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()
+22 -16
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)
@@ -150,6 +152,7 @@ class MovieSearcher(SearcherBase, MovieTypeBase):
fireEvent('notify.frontend', type = 'movie.searcher.started', data = {'id': movie['id']}, 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']:
@@ -345,7 +348,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)
@@ -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,5 +1,6 @@
from .main import Boxcar from .main import Boxcar
def start(): def start():
return Boxcar() return Boxcar()
@@ -1,5 +1,6 @@
from .main import CoreNotifier from .main import CoreNotifier
def start(): def start():
return CoreNotifier() return CoreNotifier()
+45 -25
View File
@@ -67,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):
@@ -140,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 = {}
@@ -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.trigger(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',
}, },
{ {
+1 -1
View File
@@ -40,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()
+1 -1
View File
@@ -22,7 +22,7 @@ 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:
@@ -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 = {
@@ -1,5 +1,6 @@
from .main import Pushbullet from .main import Pushbullet
def start(): def start():
return Pushbullet() return Pushbullet()
@@ -79,7 +79,7 @@ class Pushbullet(Notification):
data = self.urlopen(self.url % method, headers = headers, data = kwargs) data = self.urlopen(self.url % method, headers = headers, data = kwargs)
return json.loads(data) return json.loads(data)
except Exception, ex: except Exception as ex:
log.error('Pushbullet request failed') log.error('Pushbullet request failed')
log.debug(ex) log.debug(ex)
@@ -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()
@@ -3,6 +3,7 @@ from couchpotato.core.notifications.base import Notification
log = CPLog(__name__) log = CPLog(__name__)
class Trakt(Notification): class Trakt(Notification):
urls = { urls = {
@@ -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
@@ -1,5 +1,6 @@
from .main import XBMC from .main import XBMC
def start(): def start():
return XBMC() return XBMC()
+1 -2
View File
@@ -1,7 +1,6 @@
from couchpotato.core.helpers.variable import splitString 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 urllib2 import URLError
import base64 import base64
import json import json
import socket import socket
@@ -46,7 +45,7 @@ class XBMC(Notification):
max_successful += len(calls) max_successful += len(calls)
response = self.request(host, calls) response = self.request(host, calls)
else: else:
response = self.notifyXBMCnoJSON(host, {'title':self.default_title, 'message':message}) response = self.notifyXBMCnoJSON(host, {'title': self.default_title, 'message': message})
if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0): if data and data.get('destination_dir') and (not self.conf('only_first') or hosts.index(host) == 0):
response += self.request(host, [('VideoLibrary.Scan', {})]) response += self.request(host, [('VideoLibrary.Scan', {})])
@@ -1,5 +1,6 @@
from .main import Xmpp from .main import Xmpp
def start(): def start():
return Xmpp() return Xmpp()
@@ -1,5 +1,6 @@
from .main import Automation from .main import Automation
def start(): def start():
return Automation() return Automation()
+15 -12
View File
@@ -85,7 +85,7 @@ class Plugin(object):
class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() class_name = re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
# View path # View path
path = 'static/plugin/%s/' % (class_name) path = 'static/plugin/%s/' % class_name
# Add handler to Tornado # Add handler to Tornado
Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})]) Env.get('app').add_handlers(".*$", [(Env.get('web_base') + path + '(.*)', StaticFileHandler, {'path': static_folder})])
@@ -110,7 +110,7 @@ class Plugin(object):
f.write(content) f.write(content)
f.close() f.close()
os.chmod(path, Env.getPermission('file')) os.chmod(path, Env.getPermission('file'))
except Exception, e: except Exception as e:
log.error('Unable writing to file "%s": %s', (path, traceback.format_exc())) log.error('Unable writing to file "%s": %s', (path, traceback.format_exc()))
if os.path.isfile(path): if os.path.isfile(path):
os.remove(path) os.remove(path)
@@ -121,7 +121,7 @@ class Plugin(object):
if not os.path.isdir(path): if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder')) os.makedirs(path, Env.getPermission('folder'))
return True return True
except Exception, e: except Exception as e:
log.error('Unable to create folder "%s": %s', (path, e)) log.error('Unable to create folder "%s": %s', (path, e))
return False return False
@@ -169,7 +169,7 @@ class Plugin(object):
} }
method = 'post' if len(data) > 0 or files else 'get' method = 'post' if len(data) > 0 or files else 'get'
log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.iterkeys()] if isinstance(data, dict) else 'with data')) log.info('Opening url: %s %s, data: %s', (method, url, [x for x in data.keys()] if isinstance(data, dict) else 'with data'))
response = r.request(method, url, verify = False, **kwargs) response = r.request(method, url, verify = False, **kwargs)
data = response.content data = response.content
@@ -243,24 +243,27 @@ class Plugin(object):
except: except:
log.error("Something went wrong when finishing the plugin function. Could not find the 'is_running' key") log.error("Something went wrong when finishing the plugin function. Could not find the 'is_running' key")
def getCache(self, cache_key, url = None, **kwargs): def getCache(self, cache_key, url = None, **kwargs):
cache_key_md5 = md5(cache_key)
cache = Env.get('cache').get(cache_key_md5) use_cache = not len(kwargs.get('data', {})) > 0 and not kwargs.get('files')
if cache:
if not Env.get('dev'): log.debug('Getting cache %s', cache_key) if use_cache:
return cache cache_key_md5 = md5(cache_key)
cache = Env.get('cache').get(cache_key_md5)
if cache:
if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
return cache
if url: if url:
try: try:
cache_timeout = 300 cache_timeout = 300
if kwargs.has_key('cache_timeout'): if 'cache_timeout' in kwargs:
cache_timeout = kwargs.get('cache_timeout') cache_timeout = kwargs.get('cache_timeout')
del kwargs['cache_timeout'] del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs) data = self.urlopen(url, **kwargs)
if data and cache_timeout > 0: if data and cache_timeout > 0 and use_cache:
self.setCache(cache_key, data, timeout = cache_timeout) self.setCache(cache_key, data, timeout = cache_timeout)
return data return data
except: except:
@@ -1,5 +1,6 @@
from .main import FileBrowser from .main import FileBrowser
def start(): def start():
return FileBrowser() return FileBrowser()
+3 -2
View File
@@ -4,6 +4,7 @@ from couchpotato.core.plugins.base import Plugin
import ctypes import ctypes
import os import os
import string import string
import six
if os.name == 'nt': if os.name == 'nt':
import imp import imp
@@ -14,7 +15,7 @@ if os.name == 'nt':
raise ImportError("Missing the win32file module, which is a part of the prerequisite \ raise ImportError("Missing the win32file module, which is a part of the prerequisite \
pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/") pywin32 package. You can get it from http://sourceforge.net/projects/pywin32/files/pywin32/")
else: else:
import win32file #@UnresolvedImport import win32file #@UnresolvedImport
class FileBrowser(Plugin): class FileBrowser(Plugin):
@@ -96,7 +97,7 @@ class FileBrowser(Plugin):
def has_hidden_attribute(self, filepath): def has_hidden_attribute(self, filepath):
try: try:
attrs = ctypes.windll.kernel32.GetFileAttributesW(unicode(filepath)) #@UndefinedVariable attrs = ctypes.windll.kernel32.GetFileAttributesW(six.text_type(filepath)) #@UndefinedVariable
assert attrs != -1 assert attrs != -1
result = bool(attrs & 2) result = bool(attrs & 2)
except (AttributeError, AssertionError): except (AttributeError, AssertionError):
@@ -1,5 +1,6 @@
from .main import CategoryPlugin from .main import CategoryPlugin
def start(): def start():
return CategoryPlugin() return CategoryPlugin()
+81 -45
View File
@@ -1,3 +1,4 @@
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 addEvent from couchpotato.core.event import addEvent
@@ -41,81 +42,116 @@ class CategoryPlugin(Plugin):
for category in categories: for category in categories:
temp.append(category.to_dict()) temp.append(category.to_dict())
db.expire_all()
return temp return temp
def save(self, **kwargs): def save(self, **kwargs):
db = get_session() try:
db = get_session()
c = db.query(Category).filter_by(id = kwargs.get('id')).first() c = db.query(Category).filter_by(id = kwargs.get('id')).first()
if not c: if not c:
c = Category() c = Category()
db.add(c) db.add(c)
c.order = kwargs.get('order', c.order if c.order else 0) c.order = kwargs.get('order', c.order if c.order else 0)
c.label = toUnicode(kwargs.get('label', '')) c.label = toUnicode(kwargs.get('label', ''))
c.ignored = toUnicode(kwargs.get('ignored', '')) c.ignored = toUnicode(kwargs.get('ignored', ''))
c.preferred = toUnicode(kwargs.get('preferred', '')) c.preferred = toUnicode(kwargs.get('preferred', ''))
c.required = toUnicode(kwargs.get('required', '')) c.required = toUnicode(kwargs.get('required', ''))
c.destination = toUnicode(kwargs.get('destination', '')) c.destination = toUnicode(kwargs.get('destination', ''))
db.commit() db.commit()
category_dict = c.to_dict() category_dict = c.to_dict()
return {
'success': True,
'category': category_dict
}
except:
log.error('Failed: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return { return {
'success': True, 'success': False,
'category': category_dict 'category': None
} }
def saveOrder(self, **kwargs): def saveOrder(self, **kwargs):
db = get_session() try:
db = get_session()
order = 0 order = 0
for category_id in kwargs.get('ids', []): for category_id in kwargs.get('ids', []):
c = db.query(Category).filter_by(id = category_id).first() c = db.query(Category).filter_by(id = category_id).first()
c.order = order c.order = order
order += 1 order += 1
db.commit() db.commit()
return {
'success': True
}
except:
log.error('Failed: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
return { return {
'success': True 'success': False
} }
def delete(self, id = None, **kwargs): def delete(self, id = None, **kwargs):
db = get_session()
success = False
message = ''
try: try:
c = db.query(Category).filter_by(id = id).first() db = get_session()
db.delete(c)
db.commit()
# Force defaults on all empty category movies success = False
self.removeFromMovie(id) message = ''
try:
c = db.query(Category).filter_by(id = id).first()
db.delete(c)
db.commit()
success = True # Force defaults on all empty category movies
except Exception, e: self.removeFromMovie(id)
message = log.error('Failed deleting category: %s', e)
success = True
except Exception as e:
message = log.error('Failed deleting category: %s', e)
return {
'success': success,
'message': message
}
except:
log.error('Failed: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
db.expire_all()
return { return {
'success': success, 'success': False
'message': message
} }
def removeFromMovie(self, category_id): def removeFromMovie(self, category_id):
db = get_session() try:
movies = db.query(Media).filter(Media.category_id == category_id).all() db = get_session()
movies = db.query(Media).filter(Media.category_id == category_id).all()
if len(movies) > 0: if len(movies) > 0:
for movie in movies: for movie in movies:
movie.category_id = None movie.category_id = None
db.commit() db.commit()
except:
log.error('Failed: %s', traceback.format_exc())
db.rollback()
finally:
db.close()
@@ -1,5 +1,6 @@
from .main import Custom from .main import Custom
def start(): def start():
return Custom() return Custom()
@@ -1,5 +1,6 @@
from .main import Dashboard from .main import Dashboard
def start(): def start():
return Dashboard() return Dashboard()
+1 -1
View File
@@ -115,7 +115,7 @@ class Dashboard(Plugin):
for movie_id in movie_ids: for movie_id in movie_ids:
movies.append(movie_dict[movie_id].to_dict({ movies.append(movie_dict[movie_id].to_dict({
'library': {'titles': {}, 'files':{}}, 'library': {'titles': {}, 'files': {}},
'files': {}, 'files': {},
})) }))
@@ -1,5 +1,6 @@
from .main import FileManager from .main import FileManager
def start(): def start():
return FileManager() return FileManager()
Loaded 100 of 255 files, more files were not shown because too many files have changed in this diff. Show more