Merge branch 'refs/heads/develop' into desktop

Conflicts:
	couchpotato/core/_base/updater/main.py
This commit is contained in:
Ruud
2012-07-11 22:43:45 +02:00
362 changed files with 58929 additions and 8137 deletions
+2
View File
@@ -1 +1,3 @@
*.pyc
/data/
/_source/ /_source/
+5 -2
View File
@@ -35,7 +35,11 @@ class Loader(object):
settings.setFile(self.options.config_file) settings.setFile(self.options.config_file)
# Create data dir if needed # Create data dir if needed
self.data_dir = os.path.expanduser(Env.setting('data_dir')) if self.options.data_dir:
self.data_dir = self.options.data_dir
else:
self.data_dir = os.path.expanduser(Env.setting('data_dir'))
if self.data_dir == '': if self.data_dir == '':
self.data_dir = getDataDir() self.data_dir = getDataDir()
@@ -89,7 +93,6 @@ class Loader(object):
if self.runAsDaemon(): if self.runAsDaemon():
try: self.daemon.stop() try: self.daemon.stop()
except: pass except: pass
self.daemon.delpid()
except: except:
self.log.critical(traceback.format_exc()) self.log.critical(traceback.format_exc())
+6 -2
View File
@@ -69,10 +69,14 @@ def getApiKey():
@app.errorhandler(404) @app.errorhandler(404)
def page_not_found(error): def page_not_found(error):
index_url = url_for('web.index') index_url = url_for('web.index')
url = getattr(request, 'path')[len(index_url):] url = request.path[len(index_url):]
if url[:3] != 'api': if url[:3] != 'api':
return redirect(index_url + '#' + url) if request.path != '/':
r = request.url.replace(request.path, index_url + '#' + url)
else:
r = '%s%s' % (request.url.rstrip('/'), index_url + '#' + url)
return redirect(r)
else: else:
time.sleep(0.1) time.sleep(0.1)
return 'Wrong API key used', 404 return 'Wrong API key used', 404
+37
View File
@@ -1,10 +1,39 @@
from flask.blueprints import Blueprint from flask.blueprints import Blueprint
from flask.helpers import url_for from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect from werkzeug.utils import redirect
api = Blueprint('api', __name__) api = Blueprint('api', __name__)
api_docs = {} api_docs = {}
api_docs_missing = [] api_docs_missing = []
api_nonblock = {}
class NonBlockHandler(RequestHandler):
stoppers = []
@asynchronous
def get(self, route):
cls = NonBlockHandler
start, stop = api_nonblock[route]
cls.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
def onNewMessage(self, response):
if self.request.connection.stream.closed():
return
self.finish(response)
def on_connection_close(self):
cls = NonBlockHandler
for stop in cls.stoppers:
stop(self.onNewMessage)
cls.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):
api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs) api.add_url_rule(route + ('' if static else '/'), endpoint = route.replace('.', '::') if route else 'index', view_func = func, **kwargs)
@@ -13,6 +42,14 @@ def addApiView(route, func, static = False, docs = None, **kwargs):
else: else:
api_docs_missing.append(route) api_docs_missing.append(route)
def addNonBlockApiView(route, func_tuple, docs = None, **kwargs):
api_nonblock[route] = func_tuple
if docs:
api_docs[route[4:] if route[0:4] == 'api.' else route] = docs
else:
api_docs_missing.append(route)
""" Api view """ """ Api view """
def index(): def index():
index_url = url_for('web.index') index_url = url_for('web.index')
+35 -32
View File
@@ -5,7 +5,7 @@ from couchpotato.core.helpers.variable import cleanHost, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from flask import request from tornado.ioloop import IOLoop
from uuid import uuid4 from uuid import uuid4
import os import os
import platform import platform
@@ -18,7 +18,10 @@ log = CPLog(__name__)
class Core(Plugin): class Core(Plugin):
ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown'] ignore_restart = [
'Core.restart', 'Core.shutdown',
'Updater.check', 'Updater.autoUpdate',
]
shutdown_started = False shutdown_started = False
def __init__(self): def __init__(self):
@@ -37,12 +40,13 @@ class Core(Plugin):
'desc': 'Get version.' 'desc': 'Get version.'
}) })
addEvent('app.crappy_shutdown', self.crappyShutdown) addEvent('app.shutdown', self.shutdown)
addEvent('app.crappy_restart', self.crappyRestart) addEvent('app.restart', self.restart)
addEvent('app.load', self.launchBrowser, priority = 1) addEvent('app.load', self.launchBrowser, priority = 1)
addEvent('app.base_url', self.createBaseUrl) addEvent('app.base_url', self.createBaseUrl)
addEvent('app.api_url', self.createApiUrl) addEvent('app.api_url', self.createApiUrl)
addEvent('app.version', self.version) addEvent('app.version', self.version)
addEvent('app.load', self.checkDataDir)
addEvent('setting.save.core.password', self.md5Password) addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey) addEvent('setting.save.core.api_key', self.checkApikey)
@@ -54,39 +58,35 @@ class Core(Plugin):
def checkApikey(self, value): def checkApikey(self, value):
return value if value and len(value) > 3 else uuid4().hex return value if value and len(value) > 3 else uuid4().hex
def checkDataDir(self):
if Env.get('app_dir') in Env.get('data_dir'):
log.error('You should NOT use your CouchPotato directory to save your settings in. Files will get overwritten or be deleted.')
return True
def available(self): def available(self):
return jsonified({ return jsonified({
'succes': True 'succes': True
}) })
def crappyShutdown(self):
if self.shutdown_started:
return
try:
self.urlopen('%s/app.shutdown' % self.createApiUrl(), show_error = False)
return True
except:
self.initShutdown()
return False
def crappyRestart(self):
if self.shutdown_started:
return
try:
self.urlopen('%s/app.restart' % self.createApiUrl(), show_error = False)
return True
except:
self.initShutdown(restart = True)
return False
def shutdown(self): def shutdown(self):
self.initShutdown() if self.shutdown_started:
return False
def shutdown():
self.initShutdown()
IOLoop.instance().add_callback(shutdown)
return 'shutdown' return 'shutdown'
def restart(self): def restart(self):
self.initShutdown(restart = True) if self.shutdown_started:
return False
def restart():
self.initShutdown(restart = True)
IOLoop.instance().add_callback(restart)
return 'restarting' return 'restarting'
def initShutdown(self, restart = False): def initShutdown(self, restart = False):
@@ -102,17 +102,20 @@ class Core(Plugin):
log.debug('Every plugin got shutdown event') log.debug('Every plugin got shutdown event')
loop = True loop = True
starttime = time.time()
while loop: while loop:
log.debug('Asking who is running') log.debug('Asking who is running')
still_running = fireEvent('plugin.running', merge = True) still_running = fireEvent('plugin.running', merge = True)
log.debug('Still running: %s' % still_running) log.debug('Still running: %s', still_running)
if len(still_running) == 0: if len(still_running) == 0:
break break
elif starttime < time.time() - 30: # Always force break after 30s wait
break
running = list(set(still_running) - set(self.ignore_restart)) running = list(set(still_running) - set(self.ignore_restart))
if len(running) > 0: if len(running) > 0:
log.info('Waiting on plugins to finish: %s' % running) log.info('Waiting on plugins to finish: %s', running)
else: else:
loop = False loop = False
@@ -121,11 +124,11 @@ class Core(Plugin):
log.debug('Save to shutdown/restart') log.debug('Save to shutdown/restart')
try: try:
request.environ.get('werkzeug.server.shutdown')() IOLoop.instance().stop()
except RuntimeError: except RuntimeError:
pass pass
except: except:
log.error('Failed shutting down the server: %s' % traceback.format_exc()) log.error('Failed shutting down the server: %s', traceback.format_exc())
fireEvent('app.after_shutdown', restart = restart) fireEvent('app.after_shutdown', restart = restart)
+1 -1
View File
@@ -27,7 +27,7 @@ if Env.get('desktop'):
addEvent('app.after_shutdown', desktop.afterShutdown) addEvent('app.after_shutdown', desktop.afterShutdown)
def onClose(self, event): def onClose(self, event):
return fireEvent('app.crappy_shutdown', single = True) return fireEvent('app.shutdown', single = True)
else: else:
+5 -5
View File
@@ -28,7 +28,7 @@ class Scheduler(Plugin):
for type in ['interval', 'cron']: for type in ['interval', 'cron']:
try: try:
self.sched.unschedule_job(getattr(self, type)[identifier]['job']) self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
log.debug('%s unscheduled %s' % (type.capitalize(), identifier)) log.debug('%s unscheduled %s', (type.capitalize(), identifier))
except: except:
pass pass
@@ -45,7 +45,7 @@ class Scheduler(Plugin):
job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute']) job = self.sched.add_cron_job(cron['handle'], day = cron['day'], hour = cron['hour'], minute = cron['minute'])
cron['job'] = job cron['job'] = job
except ValueError, e: except ValueError, e:
log.error("Failed adding cronjob: %s" % e) log.error('Failed adding cronjob: %s', e)
# Intervals # Intervals
for identifier in self.intervals: for identifier in self.intervals:
@@ -55,7 +55,7 @@ class Scheduler(Plugin):
job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds']) job = self.sched.add_interval_job(interval['handle'], hours = interval['hours'], minutes = interval['minutes'], seconds = interval['seconds'])
interval['job'] = job interval['job'] = job
except ValueError, e: except ValueError, e:
log.error("Failed adding interval cronjob: %s" % e) log.error('Failed adding interval cronjob: %s', e)
# Start it # Start it
log.debug('Starting scheduler') log.debug('Starting scheduler')
@@ -75,7 +75,7 @@ class Scheduler(Plugin):
self.started = False self.started = False
def cron(self, identifier = '', handle = None, day = '*', hour = '*', minute = '*'): def cron(self, identifier = '', handle = None, day = '*', hour = '*', minute = '*'):
log.info('Scheduling "%s", cron: day = %s, hour = %s, minute = %s' % (identifier, day, hour, minute)) log.info('Scheduling "%s", cron: day = %s, hour = %s, minute = %s', (identifier, day, hour, minute))
self.remove(identifier) self.remove(identifier)
self.crons[identifier] = { self.crons[identifier] = {
@@ -86,7 +86,7 @@ class Scheduler(Plugin):
} }
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0): def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
log.info('Scheduling %s, interval: hours = %s, minutes = %s, seconds = %s' % (identifier, hours, minutes, seconds)) log.info('Scheduling %s, interval: hours = %s, minutes = %s, seconds = %s', (identifier, hours, minutes, seconds))
self.remove(identifier) self.remove(identifier)
self.intervals[identifier] = { self.intervals[identifier] = {
+93 -47
View File
@@ -1,5 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
@@ -19,6 +20,8 @@ log = CPLog(__name__)
class Updater(Plugin): class Updater(Plugin):
available_notified = False
def __init__(self): def __init__(self):
if Env.get('desktop'): if Env.get('desktop'):
@@ -28,7 +31,7 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.check, hours = 6) fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.check) addEvent('app.load', self.check)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
@@ -48,17 +51,36 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'} 'return': {'type': 'see updater.info'}
}) })
def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
# Notify before restarting
try:
if self.conf('notification'):
info = self.updater.info()
version_date = datetime.fromtimestamp(info['update_version']['date'])
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
except:
log.error('Failed notifying for update: %s', traceback.format_exc())
fireEventAsync('app.restart')
return True
return False
def check(self): def check(self):
if self.isDisabled(): if self.isDisabled():
return return
if self.updater.check(): if self.updater.check():
if self.conf('automatic') and not self.updater.update_failed: if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
if self.updater.doUpdate(): fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
fireEventAsync('app.crappy_restart') self.available_notified = True
else: return True
if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info()) return False
def info(self): def info(self):
return self.updater.info() return self.updater.info()
@@ -67,12 +89,22 @@ class Updater(Plugin):
return jsonified(self.updater.info()) return jsonified(self.updater.info())
def checkView(self): def checkView(self):
self.check() return jsonified({
return self.updater.getInfo() 'update_available': self.check(),
'info': self.updater.info()
})
def doUpdateView(self): def doUpdateView(self):
self.check()
if not self.updater.update_version:
log.error('Trying to update when no update is available.')
success = False
else:
success = self.updater.doUpdate()
return jsonified({ return jsonified({
'success': self.updater.doUpdate() 'success': success
}) })
@@ -107,7 +139,7 @@ class BaseUpdater(Plugin):
def deletePyc(self, only_excess = True): def deletePyc(self, only_excess = True):
for root, dirs, files in os.walk(Env.get('app_dir')): for root, dirs, files in os.walk(ss(Env.get('app_dir'))):
pyc_files = filter(lambda filename: filename.endswith('.pyc'), files) pyc_files = filter(lambda filename: filename.endswith('.pyc'), files)
py_files = set(filter(lambda filename: filename.endswith('.py'), files)) py_files = set(filter(lambda filename: filename.endswith('.py'), files))
@@ -115,11 +147,11 @@ class BaseUpdater(Plugin):
for excess_pyc_file in excess_pyc_files: for excess_pyc_file in excess_pyc_files:
full_path = os.path.join(root, excess_pyc_file) full_path = os.path.join(root, excess_pyc_file)
log.debug('Removing old PYC file: %s' % full_path) log.debug('Removing old PYC file: %s', full_path)
try: try:
os.remove(full_path) os.remove(full_path)
except: except:
log.error('Couldn\'t remove %s: %s' % (full_path, traceback.format_exc())) log.error('Couldn\'t remove %s: %s', (full_path, traceback.format_exc()))
for dir_name in dirs: for dir_name in dirs:
full_path = os.path.join(root, dir_name) full_path = os.path.join(root, dir_name)
@@ -127,7 +159,7 @@ class BaseUpdater(Plugin):
try: try:
os.rmdir(full_path) os.rmdir(full_path)
except: except:
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()))
@@ -137,24 +169,20 @@ class GitUpdater(BaseUpdater):
self.repo = LocalRepository(Env.get('app_dir'), command = git_command) self.repo = LocalRepository(Env.get('app_dir'), command = git_command)
def doUpdate(self): def doUpdate(self):
try: try:
log.debug('Stashing local changes') log.debug('Stashing local changes')
self.repo.saveStash() self.repo.saveStash()
log.info('Updating to latest version') log.info('Updating to latest version')
info = self.info()
self.repo.pull() self.repo.pull()
# Delete leftover .pyc files # Delete leftover .pyc files
self.deletePyc() self.deletePyc()
# Notify before returning and restarting
version_date = datetime.fromtimestamp(info['update_version']['date'])
fireEvent('updater.updated', 'Updated to a new version with hash "%s", this version is from %s' % (info['update_version']['hash'], version_date), data = info)
return True return True
except: except:
log.error('Failed updating via GIT: %s' % traceback.format_exc()) log.error('Failed updating via GIT: %s', traceback.format_exc())
self.update_failed = True self.update_failed = True
@@ -165,14 +193,14 @@ 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 = {
'hash': output.hash[:8], 'hash': output.hash[:8],
'date': output.getDate(), 'date': output.getDate(),
'type': 'git', 'type': 'git',
} }
except Exception, e: except Exception, 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'
return self.version return self.version
@@ -182,7 +210,7 @@ class GitUpdater(BaseUpdater):
if self.update_version: if self.update_version:
return return
log.info('Checking for new version on github for %s' % self.repo_name) log.info('Checking for new version on github for %s', self.repo_name)
if not Env.get('dev'): if not Env.get('dev'):
self.repo.fetch() self.repo.fetch()
@@ -194,7 +222,7 @@ class GitUpdater(BaseUpdater):
local = self.repo.getHead() local = self.repo.getHead()
remote = branch.getHead() remote = branch.getHead()
log.info('Versions, local:%s, remote:%s' % (local.hash[:8], remote.hash[:8])) log.info('Versions, local:%s, remote:%s', (local.hash[:8], remote.hash[:8]))
if local.getDate() < remote.getDate(): if local.getDate() < remote.getDate():
self.update_version = { self.update_version = {
@@ -237,24 +265,24 @@ class SourceUpdater(BaseUpdater):
tar.close() tar.close()
os.remove(destination) os.remove(destination)
self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])) if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
self.removeDir(extracted_path) self.removeDir(extracted_path)
# Write update version to file # Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version)) self.createFile(self.version_file, json.dumps(self.update_version))
return True return True
except: except:
log.error('Failed updating: %s' % traceback.format_exc()) log.error('Failed updating: %s', traceback.format_exc())
self.update_failed = True self.update_failed = True
return False return False
def replaceWith(self, path): def replaceWith(self, path):
app_dir = Env.get('app_dir') app_dir = ss(Env.get('app_dir'))
# Get list of files we want to overwrite # Get list of files we want to overwrite
self.deletePyc(only_excess = False) self.deletePyc()
existing_files = [] existing_files = []
for root, subfiles, filenames in os.walk(app_dir): for root, subfiles, filenames in os.walk(app_dir):
for filename in filenames: for filename in filenames:
@@ -267,18 +295,30 @@ class SourceUpdater(BaseUpdater):
if not Env.get('dev'): if not Env.get('dev'):
try: try:
os.remove(tofile) if os.path.isfile(tofile):
except: os.remove(tofile)
pass
try: dirname = os.path.dirname(tofile)
os.renames(fromfile, tofile) if not os.path.isdir(dirname):
self.makeDir(dirname)
os.rename(fromfile, tofile)
try: try:
existing_files.remove(tofile) existing_files.remove(tofile)
except ValueError: except ValueError:
pass pass
except Exception, e: except:
log.error('Failed overwriting file: %s' % e) log.error('Failed overwriting file "%s": %s', (tofile, traceback.format_exc()))
return False
if Env.get('app_dir') not in Env.get('data_dir'):
for still_exists in existing_files:
try:
os.remove(still_exists)
except:
log.error('Failed removing non-used file: %s', traceback.format_exc())
return True
def removeDir(self, path): def removeDir(self, path):
@@ -297,11 +337,11 @@ class SourceUpdater(BaseUpdater):
output = json.loads(f.read()) output = json.loads(f.read())
f.close() f.close()
log.debug('Source version output: %s' % output) log.debug('Source version output: %s', output)
self.version = output self.version = output
self.version['type'] = 'source' self.version['type'] = 'source'
except Exception, e: except Exception, e:
log.error('Failed using source updater. %s' % e) log.error('Failed using source updater. %s', e)
return {} return {}
return self.version return self.version
@@ -318,7 +358,7 @@ class SourceUpdater(BaseUpdater):
self.last_check = time.time() self.last_check = time.time()
except: except:
log.error('Failed updating via source: %s' % traceback.format_exc()) log.error('Failed updating via source: %s', traceback.format_exc())
return self.update_version is not None return self.update_version is not None
@@ -333,12 +373,12 @@ class SourceUpdater(BaseUpdater):
'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())
return {} return {}
class DesktopUpdater(Plugin): class DesktopUpdater(BaseUpdater):
version = None version = None
update_failed = False update_failed = False
@@ -350,9 +390,15 @@ class DesktopUpdater(Plugin):
def doUpdate(self): def doUpdate(self):
try: try:
self.desktop.CheckForUpdate(silentUnlessUpdate = True) def do_restart(e):
if e['status'] == 'done':
fireEventAsync('app.restart')
else:
log.error('Failed updating desktop: %s', e['exception'])
self.update_failed = True
self.desktop._esky.auto_update(callback = do_restart)
except: except:
log.error('Failed updating desktop: %s' % traceback.format_exc())
self.update_failed = True self.update_failed = True
return False return False
@@ -379,7 +425,7 @@ class DesktopUpdater(Plugin):
self.last_check = time.time() self.last_check = time.time()
except: except:
log.error('Failed updating desktop: %s' % traceback.format_exc()) log.error('Failed updating desktop: %s', traceback.format_exc())
return self.update_version is not None return self.update_version is not None
@@ -16,7 +16,17 @@ var UpdaterBase = new Class({
var self = this; var self = this;
Api.request('updater.check', { Api.request('updater.check', {
'onComplete': onComplete || Function.from() 'onComplete': function(json){
if(onComplete)
onComplete(json);
if(json.update_available)
self.doUpdate();
else {
App.unBlockPage();
App.fireEvent('message', 'No updates available');
}
}
}) })
}, },
@@ -52,7 +62,7 @@ var UpdaterBase = new Class({
createMessage: function(data){ createMessage: function(data){
var self = this; var self = this;
var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.update_version.hash; var changelog = 'https://github.com/'+data.repo_name+'/compare/'+data.version.hash+'...'+data.branch;
if(data.update_version.changelog) if(data.update_version.changelog)
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
@@ -81,13 +91,19 @@ var UpdaterBase = new Class({
Api.request('updater.update', { Api.request('updater.update', {
'onComplete': function(json){ 'onComplete': function(json){
if(json.success){ if(json.success){
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating'); self.updating();
App.checkAvailable.delay(500, App);
if(self.message)
self.message.destroy();
} }
} }
}); });
},
updating: function(){
App.blockPage('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App, [1000, function(){
window.location.reload();
}]);
if(self.message)
self.message.destroy();
} }
}); });
+1 -1
View File
@@ -23,7 +23,7 @@ class Downloader(Plugin):
def createFileName(self, data, filedata, movie): def createFileName(self, data, filedata, movie):
name = os.path.join(self.createNzbName(data, movie)) name = os.path.join(self.createNzbName(data, movie))
if data.get('type') == 'nzb' and "DOCTYPE nzb" not in filedata: if data.get('type') == 'nzb' and 'DOCTYPE nzb' not in filedata and '</nzb>' not in filedata:
return '%s.%s' % (name, 'rar') return '%s.%s' % (name, 'rar')
return '%s.%s' % (name, data.get('type')) return '%s.%s' % (name, data.get('type'))
@@ -30,7 +30,7 @@ config = [{
'label': 'Use for', 'label': 'Use for',
'default': 'both', 'default': 'both',
'type': 'dropdown', 'type': 'dropdown',
'values': [('nzbs & torrents', 'both'), ('nzb', 'nzb'), ('torrent', 'torrent')], 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
}, },
{ {
'name': 'manual', 'name': 'manual',
+8 -10
View File
@@ -10,38 +10,36 @@ class Blackhole(Downloader):
type = ['nzb', 'torrent'] type = ['nzb', 'torrent']
def download(self, data = {}, movie = {}, manual = False): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])): if self.isDisabled(manual) or (not self.isCorrectType(data.get('type')) or (not self.conf('use_for') in ['both', data.get('type')])):
return return
directory = self.conf('directory') directory = self.conf('directory')
if not directory or not os.path.isdir(directory): if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.' % data.get('type')) log.error('No directory set for blackhole %s download.', data.get('type'))
else: else:
try: try:
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) if not filedata or len(filedata) < 50:
log.error('No nzb/torrent available!')
if len(filedata) < 50:
log.error('No nzb available!')
return False return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie)) fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
try: try:
if not os.path.isfile(fullPath): if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.' % (data.get('type'), fullPath)) log.info('Downloading %s to %s.', (data.get('type'), fullPath))
with open(fullPath, 'wb') as f: with open(fullPath, 'wb') as f:
f.write(filedata) f.write(filedata)
return True return True
else: else:
log.info('File %s already exists.' % fullPath) log.info('File %s already exists.', fullPath)
return True return True
except: except:
log.error('Failed to download to blackhole %s' % traceback.format_exc()) log.error('Failed to download to blackhole %s', traceback.format_exc())
pass pass
except: except:
log.debug('Failed to download file %s: %s' % (data.get('name'), traceback.format_exc())) log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False return False
return False return False
+8 -17
View File
@@ -14,12 +14,16 @@ class NZBGet(Downloader):
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc' url = 'http://nzbget:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, manual = False): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return return
log.info('Sending "%s" to NZBGet.' % data.get('name')) if not filedata:
log.error('Unable to get NZB file: %s', traceback.format_exc())
return False
log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
nzb_name = '%s.nzb' % self.createNzbName(data, movie) nzb_name = '%s.nzb' % self.createNzbName(data, movie)
@@ -37,25 +41,12 @@ class NZBGet(Downloader):
if e.errcode == 401: if e.errcode == 401:
log.error('Password is incorrect.') log.error('Password is incorrect.')
else: else:
log.error('Protocol Error: %s' % e) log.error('Protocol Error: %s', e)
return False
try:
if isfunction(data.get('download')):
filedata = data.get('download')()
if not filedata:
log.error('Failed download file: %s' % nzb_name)
return False
else:
log.info('Downloading: %s' % data.get('url'))
filedata = self.urlopen(data.get('url'))
except:
log.error('Unable to get NZB file: %s' % traceback.format_exc())
return False return False
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())): if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
log.info('NZB sent successfully to NZBGet') log.info('NZB sent successfully to NZBGet')
return True return True
else: else:
log.error('NZBGet could not add %s to the queue.' % nzb_name) log.error('NZBGet could not add %s to the queue.', nzb_name)
return False return False
@@ -0,0 +1,37 @@
from .main import Pneumatic
def start():
return Pneumatic()
config = [{
'name': 'pneumatic',
'order': 30,
'groups': [
{
'tab': 'downloaders',
'name': 'pneumatic',
'label': 'Pneumatic',
'description': 'Download the .strm file to a specific folder.',
'wizard': True,
'options': [
{
'name': 'enabled',
'default': 0,
'type': 'enabler',
},
{
'name': 'directory',
'type': 'directory',
'description': 'Directory where the .strm file is saved to.',
},
{
'name': 'manual',
'default': 0,
'type': 'bool',
'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
},
],
}
],
}]
@@ -0,0 +1,56 @@
from __future__ import with_statement
from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.logger import CPLog
import os
import traceback
log = CPLog(__name__)
class Pneumatic(Downloader):
type = ['nzb']
strm_syntax = 'plugin://plugin.program.pneumatic/?mode=strm&type=add_file&nzb=%s&nzbname=%s'
def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or (not self.isCorrectType(data.get('type'))):
return
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
log.error('No directory set for .strm downloads.')
else:
try:
if not filedata or len(filedata) < 50:
log.error('No nzb available!')
return False
fullPath = os.path.join(directory, self.createFileName(data, filedata, movie))
try:
if not os.path.isfile(fullPath):
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
with open(fullPath, 'wb') as f:
f.write(filedata)
nzb_name = self.createNzbName(data, movie)
strm_path = os.path.join(directory, nzb_name)
strm_file = open(strm_path + '.strm', 'wb')
strmContent = self.strm_syntax % (fullPath, nzb_name)
strm_file.write(strmContent)
strm_file.close()
return True
else:
log.info('File %s already exists.', fullPath)
return True
except:
log.error('Failed to download .strm: %s', traceback.format_exc())
pass
except:
log.info('Failed to download file %s: %s', (data.get('name'), traceback.format_exc()))
return False
return False
@@ -33,12 +33,6 @@ config = [{
'label': 'Category', 'label': 'Category',
'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>', 'description': 'The category CP places the nzb in. Like <strong>movies</strong> or <strong>couchpotato</strong>',
}, },
{
'advanced': True,
'name': 'pp_directory',
'type': 'directory',
'description': 'Your Post-Processing Script directory, set in Sabnzbd > Config > Directories.',
},
{ {
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
+8 -76
View File
@@ -2,11 +2,6 @@ from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from inspect import ismethod, isfunction
from tempfile import mkstemp
import base64
import os
import re
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -15,25 +10,12 @@ class Sabnzbd(Downloader):
type = ['nzb'] type = ['nzb']
def download(self, data = {}, movie = {}, manual = False): def download(self, data = {}, movie = {}, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return return
log.info("Sending '%s' to SABnzbd." % data.get('name')) log.info('Sending "%s" to SABnzbd.', data.get('name'))
if self.conf('ppDir') and data.get('imdb_id'):
try:
pp_script_fn = self.buildPp(data.get('imdb_id'))
except:
log.info("Failed to create post-processing script.")
pp_script_fn = False
if not pp_script_fn:
pp = False
else:
pp = True
else:
pp = False
params = { params = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
@@ -42,29 +24,24 @@ class Sabnzbd(Downloader):
'nzbname': self.createNzbName(data, movie), 'nzbname': self.createNzbName(data, movie),
} }
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))): if filedata:
nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) if len(filedata) < 50:
log.error('No proper nzb available!')
if not nzb_file or len(nzb_file) < 50:
log.error('No nzb available!')
return False return False
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
nzb_filename = self.createFileName(data, nzb_file, movie) nzb_filename = self.createFileName(data, filedata, movie)
params['mode'] = 'addfile' params['mode'] = 'addfile'
else: else:
params['name'] = data.get('url') params['name'] = data.get('url')
if pp:
params['script'] = pp_script_fn
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params) url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try: try:
if params.get('mode') is 'addfile': if params.get('mode') is 'addfile':
data = self.urlopen(url, params = {"nzbfile": (nzb_filename, nzb_file)}, multipart = True, show_error = False) data = self.urlopen(url, timeout = 60, params = {"nzbfile": (nzb_filename, filedata)}, multipart = True, show_error = False)
else: else:
data = self.urlopen(url, show_error = False) data = self.urlopen(url, timeout = 60, show_error = False)
except: except:
log.error(traceback.format_exc()) log.error(traceback.format_exc())
return False return False
@@ -84,48 +61,3 @@ class Sabnzbd(Downloader):
else: else:
log.error("Unknown error: " + result[:40]) log.error("Unknown error: " + result[:40])
return False return False
def buildPp(self, imdb_id):
pp_script_path = self.getPpFile()
scriptB64 = '''IyEvdXNyL2Jpbi9weXRob24KaW1wb3J0IG9zCmltcG9ydCBzeXMKcHJpbnQgIkNyZWF0aW5nIGNwLmNw
bmZvIGZvciAlcyIgJSBzeXMuYXJndlsxXQppbWRiSWQgPSB7W0lNREJJREhFUkVdfQpwYXRoID0gb3Mu
cGF0aC5qb2luKHN5cy5hcmd2WzFdLCAiY3AuY3BuZm8iKQp0cnk6CiBmID0gb3BlbihwYXRoLCAndycp
CmV4Y2VwdCBJT0Vycm9yOgogcHJpbnQgIlVuYWJsZSB0byBvcGVuICVzIGZvciB3cml0aW5nIiAlIHBh
dGgKIHN5cy5leGl0KDEpCnRyeToKIGYud3JpdGUob3MucGF0aC5iYXNlbmFtZShzeXMuYXJndlswXSkr
IlxuIitpbWRiSWQpCmV4Y2VwdDoKIHByaW50ICJVbmFibGUgdG8gd3JpdGUgdG8gZmlsZTogJXMiICUg
cGF0aAogc3lzLmV4aXQoMikKZi5jbG9zZSgpCnByaW50ICJXcm90ZSBpbWRiIGlkLCAlcywgdG8gZmls
ZTogJXMiICUgKGltZGJJZCwgcGF0aCkK'''
script = re.sub(r"\{\[IMDBIDHERE\]\}", "'%s'" % imdb_id, base64.b64decode(scriptB64))
try:
f = open(pp_script_path, 'wb')
except:
log.info("Unable to open post-processing script for writing. Check permissions: %s" % pp_script_path)
return False
try:
f.write(script)
f.close()
except:
log.info("Unable to write to post-processing script. Check permissions: %s" % pp_script_path)
return False
log.info("Wrote post-processing script to: %s" % pp_script_path)
return os.path.basename(pp_script_path)
def getPpFile(self):
pp_script_handle, pp_script_path = mkstemp(suffix = '.py', dir = self.conf('ppDir'))
pp_sh = os.fdopen(pp_script_handle)
pp_sh.close()
try:
os.chmod(pp_script_path, int('777', 8))
except:
log.info("Unable to set post-processing script permissions to 777 (may still work correctly): %s" % pp_script_path)
return pp_script_path
@@ -34,6 +34,7 @@ config = [{
{ {
'name': 'paused', 'name': 'paused',
'type': 'bool', 'type': 'bool',
'default': False,
'description': 'Add the torrent paused.', 'description': 'Add the torrent paused.',
}, },
{ {
+108 -17
View File
@@ -2,48 +2,139 @@ from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from libs import transmissionrpc import httplib
import json
import re
import urllib2
log = CPLog(__name__) log = CPLog(__name__)
class Transmission(Downloader): class Transmission(Downloader):
type = ['torrent'] type = ['torrent', 'torrent_magnet']
log = CPLog(__name__)
def download(self, data = {}, movie = {}, manual = False): def download(self, data, movie, manual = False, filedata = None):
if self.isDisabled(manual) or not self.isCorrectType(data.get('type')): if self.isDisabled(manual) or not self.isCorrectType(data.get('type')):
return return
log.info('Sending "%s" to Transmission.' % data.get('name')) log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
if not isInt(host[1]): if not isInt(host[1]):
log.error("Config properties are not filled in correctly, port is missing.") log.error('Config properties are not filled in correctly, port is missing.')
return False return False
# Set parameters for Transmission # Set parameters for Transmission
params = { params = {
'paused': self.conf('paused', default = 0), 'paused': self.conf('paused', default = 0),
'download_dir': self.conf('directory', default = None) 'download-dir': self.conf('directory', default = None)
} }
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': (0 if self.conf('ratio') else 1)
}
if not filedata and data.get('type') == 'torrent':
log.error('Failed sending torrent, no data')
return False
# Send request to Transmission
try: try:
tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password')) trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id')) if data.get('type') == 'torrent_magnet':
torrent = tc.add_torrent(b64encode(filedata), **params) remote_torrent = trpc.add_torrent_uri(data.get('url'), arguments = params)
else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
# Change settings of added torrents # Change settings of added torrents
try: trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
torrent.seed_ratio_limit = self.conf('ratio')
torrent.seed_ratio_mode = 'single' if self.conf('ratio') else 'global'
except transmissionrpc.TransmissionError, e:
log.error('Failed to change settings for transfer in transmission: %s' % e)
return True return True
except Exception, err:
except transmissionrpc.TransmissionError, e: log.error('Failed to change settings for transfer: %s', err)
log.error('Failed to send link to transmission: %s' % e)
return False return False
class TransmissionRPC(object):
"""TransmissionRPC lite library"""
def __init__(self, host = 'localhost', port = 9091, username = None, password = None):
super(TransmissionRPC, self).__init__()
self.url = 'http://' + host + ':' + str(port) + '/transmission/rpc'
self.tag = 0
self.session_id = 0
self.session = {}
if username and password:
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(realm = None, uri = self.url, user = username, passwd = password)
opener = urllib2.build_opener(urllib2.HTTPBasicAuthHandler(password_manager), urllib2.HTTPDigestAuthHandler(password_manager))
opener.addheaders = [('User-agent', 'couchpotato-transmission-client/1.0')]
urllib2.install_opener(opener)
elif username or password:
log.debug('User or password missing, not using authentication.')
self.session = self.get_session()
def _request(self, ojson):
self.tag += 1
headers = {'x-transmission-session-id': str(self.session_id)}
request = urllib2.Request(self.url, json.dumps(ojson).encode('utf-8'), headers)
try:
open_request = urllib2.urlopen(request)
response = json.loads(open_request.read())
log.debug('response: %s', json.dumps(response))
if response['result'] == 'success':
log.debug('Transmission action successfull')
return response['arguments']
else:
log.debug('Unknown failure sending command to Transmission. Return text is: %s', response['result'])
return False
except httplib.InvalidURL, err:
log.error('Invalid Transmission host, check your config %s', err)
return False
except urllib2.HTTPError, err:
if err.code == 401:
log.error('Invalid Transmission Username or Password, check your config')
return False
elif err.code == 409:
msg = str(err.read())
try:
self.session_id = \
re.search('X-Transmission-Session-Id:\s*(\w+)', msg).group(1)
log.debug('X-Transmission-Session-Id: %s', self.session_id)
# #resend request with the updated header
return self._request(ojson)
except:
log.error('Unable to get Transmission Session-Id %s', err)
else:
log.error('TransmissionRPC HTTPError: %s', err)
except urllib2.URLError, err:
log.error('Unable to connect to Transmission %s', err)
def get_session(self):
post_data = {'method': 'session-get', 'tag': self.tag}
return self._request(post_data)
def add_torrent_uri(self, torrent, arguments):
arguments['filename'] = torrent
post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
return self._request(post_data)
def add_torrent_file(self, torrent, arguments):
arguments['metainfo'] = torrent
post_data = {'arguments': arguments, 'method': 'torrent-add', 'tag': self.tag}
return self._request(post_data)
def set_torrent(self, torrent_id, arguments):
arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
return self._request(post_data)
+20 -14
View File
@@ -12,7 +12,7 @@ def runHandler(name, handler, *args, **kwargs):
return handler(*args, **kwargs) return handler(*args, **kwargs)
except: except:
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())) log.error('Error in event "%s", that wasn\'t caught: %s%s', (name, traceback.format_exc(), Env.all()))
def addEvent(name, handler, priority = 100): def addEvent(name, handler, priority = 100):
@@ -43,7 +43,7 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.get(name): return if not events.get(name): return
#log.debug('Firing event %s' % name) #log.debug('Firing event %s', name)
try: try:
# Fire after event # Fire after event
@@ -53,6 +53,13 @@ def fireEvent(name, *args, **kwargs):
is_after_event = True is_after_event = True
except: pass except: pass
# onComplete event
on_complete = False
try:
on_complete = kwargs['on_complete']
del kwargs['on_complete']
except: pass
# Return single handler # Return single handler
single = False single = False
try: try:
@@ -93,7 +100,7 @@ def fireEvent(name, *args, **kwargs):
elif r[1]: elif r[1]:
errorHandler(r[1]) errorHandler(r[1])
else: else:
log.debug('Assume disabled eventhandler for: %s' % name) log.debug('Assume disabled eventhandler for: %s', name)
else: else:
results = [] results = []
@@ -123,30 +130,29 @@ def fireEvent(name, *args, **kwargs):
modified_results = fireEvent('result.modify.%s' % name, results, single = True) modified_results = fireEvent('result.modify.%s' % name, results, single = True)
if modified_results: if modified_results:
log.debug('Return modified results for %s' % name) log.debug('Return modified results for %s', name)
results = modified_results results = modified_results
if not is_after_event: if not is_after_event:
fireEvent('%s.after' % name, is_after_event = True) fireEvent('%s.after' % name, is_after_event = True)
if on_complete:
on_complete()
return results return results
except KeyError, e: except KeyError, e:
pass pass
except Exception: except Exception:
log.error('%s: %s' % (name, traceback.format_exc())) log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(name, *args, **kwargs): def fireEventAsync(*args, **kwargs):
#log.debug('Async "%s": %s, %s' % (name, args, kwargs))
try: try:
e = events[name] my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
e.lock.acquire() my_thread.setDaemon(True)
e.asynchronous = True my_thread.start()
e.error_handler = errorHandler
e(*args, **kwargs)
e.lock.release()
return True return True
except Exception, e: except Exception, e:
log.error('%s: %s' % (name, e)) log.error('%s: %s', (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error
+5 -1
View File
@@ -31,10 +31,14 @@ def toUnicode(original, *args):
except: except:
raise raise
except UnicodeDecodeError: except UnicodeDecodeError:
log.error('Unable to decode value: %s... ' % repr(original)[:20]) log.error('Unable to decode value: %s... ', repr(original)[:20])
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):
from couchpotato.environment import Env
return toUnicode(original, *args).encode(Env.get('encoding'))
def ek(original, *args): def ek(original, *args):
if isinstance(original, (str, unicode)): if isinstance(original, (str, unicode)):
try: try:
+11 -7
View File
@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp from couchpotato.core.helpers.variable import natcmp
from flask.globals import current_app from flask.globals import current_app
from flask.helpers import json from flask.helpers import json, make_response
from libs.werkzeug.urls import url_decode from libs.werkzeug.urls import url_decode
from urllib import unquote from urllib import unquote
import flask import flask
@@ -26,7 +26,7 @@ def getParams():
for item in nested: for item in nested:
if item is nested[-1]: if item is nested[-1]:
current[item] = toUnicode(unquote(value)).encode('utf-8') current[item] = toUnicode(unquote(value))
else: else:
try: try:
current[item] current[item]
@@ -35,7 +35,7 @@ def getParams():
current = current[item] current = current[item]
else: else:
temp[param] = toUnicode(unquote(value)).encode('utf-8') temp[param] = toUnicode(unquote(value))
return dictToList(temp) return dictToList(temp)
@@ -57,7 +57,7 @@ def dictToList(params):
def getParam(attr, default = None): def getParam(attr, default = None):
try: try:
return toUnicode(unquote(getattr(flask.request, 'args').get(attr, default))).encode('utf-8') return toUnicode(unquote(getattr(flask.request, 'args').get(attr, default)))
except: except:
return default return default
@@ -70,9 +70,13 @@ def jsonify(mimetype, *args, **kwargs):
return getattr(current_app, 'response_class')(content, mimetype = mimetype) return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs): def jsonified(*args, **kwargs):
from couchpotato.environment import Env
callback = getParam('callback_func', None) callback = getParam('callback_func', None)
if callback: if callback:
return padded_jsonify(callback, *args, **kwargs) content = padded_jsonify(callback, *args, **kwargs)
else: else:
return jsonify('text/javascript' if Env.doDebug() else 'application/json', *args, **kwargs) content = jsonify('application/json', *args, **kwargs)
response = make_response(content)
response.cache_control.no_cache = True
return response
+1 -1
View File
@@ -47,5 +47,5 @@ class RSS(object):
try: try:
return XMLTree.parse(data).findall(path) return XMLTree.parse(data).findall(path)
except Exception, e: except Exception, e:
log.error('Error parsing RSS. %s' % e) log.error('Error parsing RSS. %s', e)
return [] return []
+15 -5
View File
@@ -2,7 +2,10 @@ from couchpotato.core.logger import CPLog
import hashlib import hashlib
import os.path import os.path
import platform import platform
import random
import re import re
import string
import sys
log = CPLog(__name__) log = CPLog(__name__)
@@ -20,6 +23,10 @@ def getDataDir():
if 'darwin' in platform.platform().lower(): if 'darwin' in platform.platform().lower():
return os.path.join(user_dir, 'Library', 'Application Support', 'CouchPotato') return os.path.join(user_dir, 'Library', 'Application Support', 'CouchPotato')
# FreeBSD
if 'freebsd' in sys.platform:
return os.path.join('/usr/local/', 'couchpotato', 'data')
# Linux # Linux
return os.path.join(user_dir, '.couchpotato') return os.path.join(user_dir, '.couchpotato')
@@ -77,9 +84,9 @@ def cleanHost(host):
return host return host
def getImdb(txt): def getImdb(txt, check_inside = True):
if os.path.isfile(txt): if check_inside and os.path.isfile(txt):
output = open(txt, 'r') output = open(txt, 'r')
txt = output.read() txt = output.read()
output.close() output.close()
@@ -94,7 +101,7 @@ def getImdb(txt):
def tryInt(s): def tryInt(s):
try: return int(s) try: return int(s)
except: return s except: return 0
def tryFloat(s): def tryFloat(s):
try: return float(s) if '.' in s else tryInt(s) try: return float(s) if '.' in s else tryInt(s)
@@ -111,9 +118,12 @@ def getTitle(library_dict):
try: try:
return library_dict['titles'][0]['title'] return library_dict['titles'][0]['title']
except: except:
log.error('Could not get title for %s' % library_dict['identifier']) log.error('Could not get title for %s', library_dict['identifier'])
return None return None
except: except:
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 randomString(size = 8, chars = string.ascii_uppercase + string.digits):
return ''.join(random.choice(chars) for x in range(size))
+6 -6
View File
@@ -45,7 +45,7 @@ class Loader(object):
try: try:
m = getattr(self.loadModule(module_name), plugin.get('name')) m = getattr(self.loadModule(module_name), plugin.get('name'))
log.info("Loading %s: %s" % (plugin['type'], plugin['name'])) log.info('Loading %s: %s', (plugin['type'], plugin['name']))
# Save default settings for plugin/provider # Save default settings for plugin/provider
did_save += self.loadSettings(m, module_name, save = False) did_save += self.loadSettings(m, module_name, save = False)
@@ -57,10 +57,10 @@ class Loader(object):
log.error(e.message) log.error(e.message)
pass pass
# todo:: this needs to be more descriptive. # todo:: this needs to be more descriptive.
log.error('Import error, remove the empty folder: %s' % plugin.get('module')) log.error('Import error, remove the empty folder: %s', plugin.get('module'))
log.debug('Can\'t import %s: %s' % (module_name, traceback.format_exc())) log.debug('Can\'t import %s: %s', (module_name, traceback.format_exc()))
except: except:
log.error('Can\'t import %s: %s' % (module_name, traceback.format_exc())) log.error('Can\'t import %s: %s', (module_name, traceback.format_exc()))
if did_save: if did_save:
fireEvent('settings.save') fireEvent('settings.save')
@@ -84,7 +84,7 @@ class Loader(object):
fireEvent('settings.register', section_name = section['name'], options = options, save = save) fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True return True
except: except:
log.debug("Failed loading settings for '%s': %s" % (name, traceback.format_exc())) log.debug('Failed loading settings for "%s": %s', (name, traceback.format_exc()))
return False return False
def loadPlugins(self, module, name): def loadPlugins(self, module, name):
@@ -97,7 +97,7 @@ class Loader(object):
return True return True
except Exception, e: except Exception, e:
log.error("Failed loading plugin '%s': %s" % (module.__file__, traceback.format_exc())) log.error('Failed loading plugin "%s": %s', (module.__file__, traceback.format_exc()))
return False return False
def addModule(self, priority, plugin_type, module, name): def addModule(self, priority, plugin_type, module, name):
+29 -17
View File
@@ -1,5 +1,6 @@
import logging import logging
import re import re
import traceback
class CPLog(object): class CPLog(object):
@@ -13,31 +14,42 @@ class CPLog(object):
self.context = context self.context = context
self.logger = logging.getLogger() self.logger = logging.getLogger()
def info(self, msg): def info(self, msg, replace_tuple = ()):
self.logger.info(self.addContext(msg)) self.logger.info(self.addContext(msg, replace_tuple))
def debug(self, msg): def debug(self, msg, replace_tuple = ()):
self.logger.debug(self.addContext(msg)) self.logger.debug(self.addContext(msg, replace_tuple))
def error(self, msg): def error(self, msg, replace_tuple = ()):
self.logger.error(self.addContext(msg)) self.logger.error(self.addContext(msg, replace_tuple))
def warning(self, msg): def warning(self, msg, replace_tuple = ()):
self.logger.warning(self.addContext(msg)) self.logger.warning(self.addContext(msg, replace_tuple))
def critical(self, msg): def critical(self, msg, replace_tuple = ()):
self.logger.critical(self.addContext(msg), exc_info = 1) self.logger.critical(self.addContext(msg, replace_tuple), exc_info = 1)
def addContext(self, msg): def addContext(self, msg, replace_tuple = ()):
return '[%+25.25s] %s' % (self.context[-25:], self.removePrivateData(msg)) return '[%+25.25s] %s' % (self.context[-25:], self.safeMessage(msg, replace_tuple))
def removePrivateData(self, msg): def safeMessage(self, msg, replace_tuple = ()):
try:
msg = unicode(msg)
except:
pass
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.encoding import ss
msg = ss(msg)
try:
msg = msg % replace_tuple
except:
try:
if isinstance(replace_tuple, tuple):
msg = msg % tuple([ss(x) for x in list(replace_tuple)])
else:
msg = msg % ss(replace_tuple)
except:
self.logger.error(u'Failed encoding stuff to log: %s' % traceback.format_exc())
if not Env.get('dev'): if not Env.get('dev'):
for replace in self.replace_private: for replace in self.replace_private:
+5 -2
View File
@@ -13,7 +13,10 @@ class Notification(Plugin):
default_title = Env.get('appname') default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!' test_message = 'ZOMG Lazors Pewpewpew!'
listen_to = ['movie.downloaded', 'movie.snatched', 'updater.available'] listen_to = [
'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated',
]
dont_listen_to = [] dont_listen_to = []
def __init__(self): def __init__(self):
@@ -41,7 +44,7 @@ class Notification(Plugin):
test_type = self.testNotifyName() test_type = self.testNotifyName()
log.info('Sending test to %s' % test_type) log.info('Sending test to %s', test_type)
success = self.notify( success = self.notify(
message = self.test_message, message = self.test_message,
+95 -21
View File
@@ -1,6 +1,6 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
@@ -8,14 +8,19 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading
import time import time
import uuid
log = CPLog(__name__) log = CPLog(__name__)
class CoreNotifier(Notification): class CoreNotifier(Notification):
m_lock = threading.Lock()
messages = [] messages = []
listeners = []
listen_to = [ listen_to = [
'movie.downloaded', 'movie.snatched', 'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
@@ -30,7 +35,7 @@ class CoreNotifier(Notification):
addApiView('notification.markread', self.markAsRead, docs = { addApiView('notification.markread', self.markAsRead, docs = {
'desc': 'Mark notifications as read', 'desc': 'Mark notifications as read',
'params': { 'params': {
'ids': {'desc': 'Notification id you want to mark as read.', 'type': 'int (comma separated)'}, 'ids': {'desc': 'Notification id you want to mark as read. All if ids is empty.', 'type': 'int (comma separated)'},
}, },
}) })
@@ -46,22 +51,31 @@ class CoreNotifier(Notification):
}"""} }"""}
}) })
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener) addApiView('notification.listener', self.listener)
self.registerEvents() addEvent('app.load', self.clean)
def registerEvents(self): def clean(self):
db = get_session()
db.query(Notif).filter(Notif.added <= (int(time.time()) - 2419200)).delete()
db.commit()
# Library update, frontend refresh
addEvent('library.update_finish', lambda data: fireEvent('notify.frontend', type = 'library.update', data = data))
def markAsRead(self): def markAsRead(self):
ids = [x.strip() for x in getParam('ids').split(',')]
ids = None
if getParam('ids'):
ids = [x.strip() for x in getParam('ids').split(',')]
db = get_session() db = get_session()
q = db.query(Notif) \ if ids:
.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:
q = db.query(Notif).filter_by(read = False)
q.update({Notif.read: True}) q.update({Notif.read: True})
db.commit() db.commit()
@@ -83,6 +97,8 @@ class CoreNotifier(Notification):
limit = splt[0] limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset) q = q.limit(limit).offset(offset)
else:
q = q.limit(200)
results = q.all() results = q.all()
notifications = [] notifications = []
@@ -114,25 +130,86 @@ class CoreNotifier(Notification):
ndict = n.to_dict() ndict = n.to_dict()
ndict['type'] = 'notification' ndict['type'] = 'notification'
ndict['time'] = time.time() ndict['time'] = time.time()
self.messages.append(ndict)
self.frontend(type = listener, data = data)
#db.close() #db.close()
return True return True
def frontend(self, type = 'notification', data = {}): def frontend(self, type = 'notification', data = {}, message = None):
self.messages.append({
self.m_lock.acquire()
notification = {
'message_id': str(uuid.uuid4()),
'time': time.time(), 'time': time.time(),
'type': type, 'type': type,
'data': data, 'data': data,
}) 'message': message,
}
self.messages.append(notification)
while len(self.listeners) > 0 and not self.shuttingDown():
try:
listener, last_id = self.listeners.pop()
listener({
'success': True,
'result': [notification],
})
except:
break
self.m_lock.release()
self.cleanMessages()
def addListener(self, callback, last_id = None):
if last_id:
messages = self.getMessages(last_id)
if len(messages) > 0:
return callback({
'success': True,
'result': messages,
})
self.listeners.append((callback, last_id))
def removeListener(self, callback):
for list_tuple in self.listeners:
try:
listener, last_id = list_tuple
if listener == callback:
self.listeners.remove(list_tuple)
except:
pass
def cleanMessages(self):
self.m_lock.acquire()
for message in self.messages:
if message['time'] < (time.time() - 15):
self.messages.remove(message)
self.m_lock.release()
def getMessages(self, last_id):
self.m_lock.acquire()
recent = []
index = 0
for i in xrange(len(self.messages)):
index = len(self.messages) - i - 1
if self.messages[index]["message_id"] == last_id: break
recent = self.messages[index:]
self.m_lock.release()
return recent or []
def listener(self): def listener(self):
messages = [] messages = []
for message in self.messages:
#delete message older then 15s
if message['time'] > (time.time() - 15):
messages.append(message)
# Get unread # Get unread
if getParam('init'): if getParam('init'):
@@ -146,9 +223,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification' ndict['type'] = 'notification'
messages.append(ndict) messages.append(ndict)
#db.close()
self.messages = []
return jsonified({ return jsonified({
'success': True, 'success': True,
'result': messages, 'result': messages,
@@ -8,9 +8,10 @@ var NotificationBase = new Class({
self.setOptions(options); self.setOptions(options);
// Listener // Listener
App.addEvent('load', self.startInterval.bind(self)); App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('unload', self.stopTimer.bind(self)); App.addEvent('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self)); App.addEvent('notification', self.notify.bind(self));
App.addEvent('message', self.showMessage.bind(self));
// Add test buttons to settings page // Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self)); App.addEvent('load', self.addTestButtons.bind(self));
@@ -30,7 +31,11 @@ var NotificationBase = new Class({
'href': App.createUrl('notifications'), 'href': App.createUrl('notifications'),
'text': 'Show older notifications' 'text': 'Show older notifications'
})); */ })); */
}) });
window.addEvent('load', function(){
self.startInterval.delay(Browser.safari ? 100 : 0, self)
});
}, },
@@ -73,9 +78,6 @@ var NotificationBase = new Class({
if(ids.length > 0) if(ids.length > 0)
Api.request('notification.markread', { Api.request('notification.markread', {
'data': {
'ids': ids.join(',')
},
'onSuccess': function(){ 'onSuccess': function(){
self.setBadge('') self.setBadge('')
} }
@@ -83,39 +85,88 @@ var NotificationBase = new Class({
}, },
startInterval: function(){ startInterval: function(force){
var self = this; var self = this;
self.request = Api.request('notification.listener', { if(self.stopped && !force){
'initialDelay': 100, self.stopped = false;
'delay': 3000, return;
}
Api.request('notification.listener', {
'data': {'init':true}, 'data': {'init':true},
'onSuccess': self.processData.bind(self) 'onSuccess': self.processData.bind(self)
}) }).send()
self.request.startTimer()
}, },
startTimer: function(){ startPoll: function(){
if(this.request) var self = this;
this.request.startTimer()
if(self.stopped || (self.request && self.request.isRunning()))
return;
self.request = Api.request('nonblock/notification.listener', {
'onSuccess': self.processData.bind(self),
'data': {
'last_id': self.last_id
},
'onFailure': function(){
self.startPoll.delay(2000, self)
}
}).send()
}, },
stopTimer: function(){ stopPoll: function(){
if(this.request) if(this.request)
this.request.stopTimer() this.request.cancel()
this.stopped = true;
}, },
processData: function(json){ processData: function(json){
var self = this; var self = this;
self.request.options.data = {} // Process data
Array.each(json.result, function(result){ if(json){
App.fireEvent(result.type, result) Array.each(json.result, function(result){
}) App.fireEvent(result.type, result);
if(result.message && result.read === undefined)
self.showMessage(result.message);
})
if(json.result.length > 0)
self.last_id = json.result.getLast().message_id
}
// Restart poll
self.startPoll()
}, },
showMessage: function(message){
var self = this;
if(!self.message_container)
self.message_container = new Element('div.messages').inject(document.body);
var new_message = new Element('div.message', {
'text': message
}).inject(self.message_container);
setTimeout(function(){
new_message.addClass('show')
}, 10);
setTimeout(function(){
new_message.addClass('hide')
setTimeout(function(){
new_message.destroy();
}, 1000);
}, 4000);
},
// Notification setting tests
addTestButtons: function(){ addTestButtons: function(){
var self = this; var self = this;
+3 -4
View File
@@ -1,9 +1,8 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent, addEvent
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 couchpotato.environment import Env from couchpotato.environment import Env
from gntp import notifier from gntp import notifier
import logging
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -17,7 +16,7 @@ class Growl(Notification):
super(Growl, self).__init__() super(Growl, self).__init__()
if self.isEnabled(): if self.isEnabled():
self.register() addEvent('app.load', self.register)
def register(self): def register(self):
if self.registered: return if self.registered: return
@@ -39,7 +38,7 @@ class Growl(Notification):
self.growl.register() self.growl.register()
self.registered = True self.registered = True
except: except:
log.error('Failed register of growl: %s' % traceback.format_exc()) log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return if self.isDisabled(): return
@@ -1,6 +0,0 @@
from .main import History
def start():
return History()
config = []
@@ -1,27 +0,0 @@
from couchpotato import get_session
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import History as Hist
import time
log = CPLog(__name__)
class History(Notification):
listen_to = ['movie.downloaded', 'movie.snatched', 'renamer.canceled']
def notify(self, message = '', data = {}, listener = None):
db = get_session()
history = Hist(
added = int(time.time()),
message = toUnicode(message),
release_id = data.get('id', 0)
)
db.add(history)
db.commit()
#db.close()
return True
+8 -8
View File
@@ -33,10 +33,10 @@ class NMJ(Notification):
try: try:
terminal = telnetlib.Telnet(host) terminal = telnetlib.Telnet(host)
except Exception: except Exception:
log.error('Warning: unable to get a telnet session to %s' % (host)) log.error('Warning: unable to get a telnet session to %s', (host))
return self.failed() return self.failed()
log.debug('Connected to %s via telnet' % (host)) log.debug('Connected to %s via telnet', (host))
terminal.read_until('sh-3.00# ') terminal.read_until('sh-3.00# ')
terminal.write('cat /tmp/source\n') terminal.write('cat /tmp/source\n')
terminal.write('cat /tmp/netshare\n') terminal.write('cat /tmp/netshare\n')
@@ -48,9 +48,9 @@ class NMJ(Notification):
if match: if match:
database = match.group(1) database = match.group(1)
device = match.group(2) device = match.group(2)
log.info('Found NMJ database %s on device %s' % (database, device)) log.info('Found NMJ database %s on device %s', (database, device))
else: else:
log.error('Could not get current NMJ database on %s, NMJ is probably not running!' % (host)) log.error('Could not get current NMJ database on %s, NMJ is probably not running!', (host))
return self.failed() return self.failed()
if device.startswith('NETWORK_SHARE/'): if device.startswith('NETWORK_SHARE/'):
@@ -58,7 +58,7 @@ class NMJ(Notification):
if match: if match:
mount = match.group().replace('127.0.0.1', host) mount = match.group().replace('127.0.0.1', host)
log.info('Found mounting url on the Popcorn Hour in configuration: %s' % (mount)) log.info('Found mounting url on the Popcorn Hour in configuration: %s', (mount))
else: else:
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url') log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed() return self.failed()
@@ -77,7 +77,7 @@ class NMJ(Notification):
database = self.conf('database') database = self.conf('database')
if self.mount: if self.mount:
log.debug('Try to mount network drive via url: %s' % (mount)) log.debug('Try to mount network drive via url: %s', (mount))
try: try:
data = self.urlopen(mount) data = self.urlopen(mount)
except: except:
@@ -102,11 +102,11 @@ class NMJ(Notification):
et = etree.fromstring(response) et = etree.fromstring(response)
result = et.findtext('returnValue') result = et.findtext('returnValue')
except SyntaxError, e: except SyntaxError, 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
if int(result) > 0: if int(result) > 0:
log.error('Popcorn Hour returned an errorcode: %s' % (result)) log.error('Popcorn Hour returned an errorcode: %s', (result))
return False return False
else: else:
log.info('NMJ started background scan') log.info('NMJ started background scan')
@@ -32,7 +32,7 @@ class Notifo(Notification):
raise Exception raise Exception
except: except:
log.error('Notification failed: %s' % traceback.format_exc()) log.error('Notification failed: %s', traceback.format_exc())
return False return False
log.info('Notifo notification successful.') log.info('Notifo notification successful.')
@@ -23,6 +23,6 @@ class NotifyMyAndroid(Notification):
for key in keys: for key in keys:
if not response[str(key)]['code'] == u'200': if not response[str(key)]['code'] == 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']))
return response return response
@@ -17,7 +17,7 @@ class NotifyMyWP(Notification):
for key in keys: for key in keys:
if not response[key]['Code'] == u'200': if not response[key]['Code'] == 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
return response return response
@@ -17,8 +17,9 @@ config = [{
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'localhost:32400', 'default': 'localhost',
'description': 'Default should be on localhost:32400', 'description': 'Default should be on localhost',
'advanced': True,
}, },
], ],
} }
+30 -2
View File
@@ -1,4 +1,5 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
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
@@ -11,13 +12,14 @@ log = CPLog(__name__)
class Plex(Notification): class Plex(Notification):
def __init__(self): def __init__(self):
super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary) addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}): def addToLibrary(self, group = {}):
if self.isDisabled(): return if self.isDisabled(): return
log.info('Sending notification to Plex') log.info('Sending notification to Plex')
hosts = [cleanHost(x.strip()) for x in self.conf('host').split(",")] hosts = [cleanHost(x.strip() + ':32400') for x in self.conf('host').split(",")]
for host in hosts: for host in hosts:
@@ -36,7 +38,33 @@ class Plex(Notification):
x = self.urlopen(url) x = self.urlopen(url)
except: except:
log.error('Plex library update failed for %s: %s' % (host, traceback.format_exc())) log.error('Plex library update failed for %s: %s', (host, traceback.format_exc()))
return False return False
return True return True
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
successful = 0
for host in hosts:
if self.send({'command': 'ExecBuiltIn', 'parameter': 'Notification(CouchPotato, %s)' % message}, host):
successful += 1
return successful == len(hosts)
def send(self, command, host):
url = 'http://%s/xbmcCmds/xbmcHttp/?%s' % (host, tryUrlencode(command))
headers = {}
try:
self.urlopen(url, headers = headers, show_error = False)
except:
log.error("Couldn't sent command to Plex: %s", traceback.format_exc())
return False
log.info('Plex notification to %s successful.', host)
return True
+1 -1
View File
@@ -32,7 +32,7 @@ class Prowl(Notification):
log.info('Prowl notifications sent.') log.info('Prowl notifications sent.')
return True return True
elif request_status == 401: elif request_status == 401:
log.error('Prowl auth failed: %s' % response.reason) log.error('Prowl auth failed: %s', response.reason)
return False return False
else: else:
log.error('Prowl notification failed.') log.error('Prowl notification failed.')
@@ -35,7 +35,7 @@ class Pushover(Notification):
log.info('Pushover notifications sent.') log.info('Pushover notifications sent.')
return True return True
elif request_status == 401: elif request_status == 401:
log.error('Pushover auth failed: %s' % response.reason) log.error('Pushover auth failed: %s', response.reason)
return False return False
else: else:
log.error('Pushover notification failed.') log.error('Pushover notification failed.')
@@ -15,14 +15,14 @@ class Synoindex(Notification):
if self.isDisabled(): return if self.isDisabled(): return
command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')] command = ['/usr/syno/bin/synoindex', '-A', group.get('destination_dir')]
log.info(u'Executing synoindex command: %s ' % command) log.info(u'Executing synoindex command: %s ', command)
try: try:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT) p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
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, e:
log.error('Unable to run synoindex: %s' % e) log.error('Unable to run synoindex: %s', e)
return False return False
return True return True
@@ -55,7 +55,7 @@ class Twitter(Notification):
else: else:
api.PostUpdate('[%s] %s' % (self.default_title, message)) api.PostUpdate('[%s] %s' % (self.default_title, message))
except Exception, e: except Exception, e:
log.error('Error sending tweet: %s' % e) log.error('Error sending tweet: %s', e)
return False return False
return True return True
@@ -71,7 +71,7 @@ class Twitter(Notification):
resp, content = oauth_client.request(self.urls['request'], 'POST', body = tryUrlencode({'oauth_callback': callback_url})) resp, content = oauth_client.request(self.urls['request'], 'POST', body = tryUrlencode({'oauth_callback': callback_url}))
if resp['status'] != '200': if resp['status'] != '200':
log.error('Invalid response from Twitter requesting temp token: %s' % resp['status']) log.error('Invalid response from Twitter requesting temp token: %s', resp['status'])
return jsonified({ return jsonified({
'success': False, 'success': False,
}) })
@@ -80,7 +80,7 @@ class Twitter(Notification):
auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token']) auth_url = self.urls['authorize'] + ("?oauth_token=%s" % self.request_token['oauth_token'])
log.info('Redirecting to "%s"' % auth_url) log.info('Redirecting to "%s"', auth_url)
return jsonified({ return jsonified({
'success': True, 'success': True,
'url': auth_url, 'url': auth_url,
@@ -100,10 +100,10 @@ class Twitter(Notification):
access_token = dict(parse_qsl(content)) access_token = dict(parse_qsl(content))
if resp['status'] != '200': if resp['status'] != '200':
log.error('The request for an access token did not succeed: %s' % resp['status']) log.error('The request for an access token did not succeed: %s', resp['status'])
return 'Twitter auth failed' return 'Twitter auth failed'
else: else:
log.debug('Tokens: %s, %s' % (access_token['oauth_token'], access_token['oauth_token_secret'])) log.debug('Tokens: %s, %s', (access_token['oauth_token'], access_token['oauth_token_secret']))
self.conf('access_token_key', value = access_token['oauth_token']) self.conf('access_token_key', value = access_token['oauth_token'])
self.conf('access_token_secret', value = access_token['oauth_token_secret']) self.conf('access_token_secret', value = access_token['oauth_token_secret'])
+1 -1
View File
@@ -35,5 +35,5 @@ class XBMC(Notification):
log.error("Couldn't sent command to XBMC") log.error("Couldn't sent command to XBMC")
return False return False
log.info('XBMC notification to %s successful.' % host) log.info('XBMC notification to %s successful.', host)
return True return True
@@ -25,7 +25,7 @@ config = [{
}, },
{ {
'name': 'rating', 'name': 'rating',
'default': 6.0, 'default': 7.0,
'type': 'float', 'type': 'float',
}, },
{ {
+5 -1
View File
@@ -19,4 +19,8 @@ class Automation(Plugin):
movies = fireEvent('automation.get_movies', merge = True) movies = fireEvent('automation.get_movies', merge = True)
for imdb_id in movies: for imdb_id in movies:
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False) prop_name = 'automation.added.%s' % imdb_id
added = Env.prop(prop_name, default = False)
if not added:
fireEvent('movie.add', params = {'identifier': imdb_id}, force_readd = False)
Env.prop(prop_name, True)
+22 -14
View File
@@ -1,6 +1,6 @@
from couchpotato import addView from couchpotato import addView
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode, simplifyString, ss
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -71,6 +71,7 @@ class Plugin(object):
return send_from_directory(d, filename) return send_from_directory(d, filename)
def createFile(self, path, content, binary = False): def createFile(self, path, content, binary = False):
path = ss(path)
self.makeDir(os.path.dirname(path)) self.makeDir(os.path.dirname(path))
@@ -80,20 +81,21 @@ class Plugin(object):
f.close() f.close()
os.chmod(path, Env.getPermission('file')) os.chmod(path, Env.getPermission('file'))
except Exception, e: except Exception, e:
log.error('Unable writing to file "%s": %s' % (path, e)) log.error('Unable writing to file "%s": %s', (path, e))
def makeDir(self, path): def makeDir(self, path):
path = ss(path)
try: try:
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, 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
# http request # http request
def urlopen(self, url, timeout = 10, params = {}, headers = {}, multipart = False, show_error = True): def urlopen(self, url, timeout = 30, params = {}, headers = {}, opener = None, multipart = False, show_error = True):
# Fill in some headers # Fill in some headers
if not headers.get('Referer'): if not headers.get('Referer'):
@@ -106,7 +108,7 @@ class Plugin(object):
# Don't try for failed requests # Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0: if self.http_failed_disabled.get(host, 0) > 0:
if self.http_failed_disabled[host] > (time.time() - 900): if self.http_failed_disabled[host] > (time.time() - 900):
log.info('Disabled calls to %s for 15 minutes because so many failed requests.' % host) log.info('Disabled calls to %s for 15 minutes because so many failed requests.', host)
raise Exception raise Exception
else: else:
del self.http_failed_request[host] del self.http_failed_request[host]
@@ -116,7 +118,7 @@ class Plugin(object):
try: try:
if multipart: if multipart:
log.info('Opening multipart url: %s, params: %s' % (url, [x for x in params.iterkeys()])) log.info('Opening multipart url: %s, params: %s', (url, [x for x in params.iterkeys()]))
request = urllib2.Request(url, params, headers) request = urllib2.Request(url, params, headers)
cookies = cookielib.CookieJar() cookies = cookielib.CookieJar()
@@ -124,16 +126,19 @@ class Plugin(object):
data = opener.open(request, timeout = timeout).read() data = opener.open(request, timeout = timeout).read()
else: else:
log.info('Opening url: %s, params: %s' % (url, [x for x in params.iterkeys()])) log.info('Opening url: %s, params: %s', (url, [x for x in params.iterkeys()]))
data = tryUrlencode(params) if len(params) > 0 else None data = tryUrlencode(params) if len(params) > 0 else None
request = urllib2.Request(url, data, headers) request = urllib2.Request(url, data, headers)
data = urllib2.urlopen(request, timeout = timeout).read() if opener:
data = opener.open(request, timeout = timeout).read()
else:
data = urllib2.urlopen(request, timeout = timeout).read()
self.http_failed_request[host] = 0 self.http_failed_request[host] = 0
except IOError: except IOError:
if show_error: if show_error:
log.error('Failed opening url in %s: %s %s' % (self.getName(), url, traceback.format_exc(1))) log.error('Failed opening url in %s: %s %s', (self.getName(), url, traceback.format_exc(1)))
# Save failed requests by hosts # Save failed requests by hosts
try: try:
@@ -147,7 +152,7 @@ class Plugin(object):
self.http_failed_disabled[host] = time.time() self.http_failed_disabled[host] = time.time()
except: except:
log.debug('Failed logging failed requests for %s: %s' % (url, traceback.format_exc())) log.debug('Failed logging failed requests for %s: %s', (url, traceback.format_exc()))
raise raise
@@ -163,7 +168,7 @@ class Plugin(object):
wait = math.ceil(last_use - now + self.http_time_between_calls) wait = math.ceil(last_use - now + self.http_time_between_calls)
if wait > 0: if wait > 0:
log.debug('Waiting for %s, %d seconds' % (self.getName(), wait)) log.debug('Waiting for %s, %d seconds', (self.getName(), wait))
time.sleep(last_use - now + self.http_time_between_calls) time.sleep(last_use - now + self.http_time_between_calls)
def beforeCall(self, handler): def beforeCall(self, handler):
@@ -199,9 +204,10 @@ class Plugin(object):
def getCache(self, cache_key, url = None, **kwargs): def getCache(self, cache_key, url = None, **kwargs):
cache_key = simplifyString(cache_key)
cache = Env.get('cache').get(cache_key) cache = Env.get('cache').get(cache_key)
if cache: if cache:
if not Env.get('dev'): log.debug('Getting cache %s' % cache_key) if not Env.get('dev'): log.debug('Getting cache %s', cache_key)
return cache return cache
if url: if url:
@@ -213,13 +219,15 @@ class Plugin(object):
del kwargs['cache_timeout'] del kwargs['cache_timeout']
data = self.urlopen(url, **kwargs) data = self.urlopen(url, **kwargs)
self.setCache(cache_key, data, timeout = cache_timeout)
if data:
self.setCache(cache_key, data, timeout = cache_timeout)
return data return data
except: except:
pass pass
def setCache(self, cache_key, value, timeout = 300): def setCache(self, cache_key, value, timeout = 300):
log.debug('Setting cache %s' % cache_key) log.debug('Setting cache %s', cache_key)
Env.get('cache').set(cache_key, value, timeout) Env.get('cache').set(cache_key, value, timeout)
return value return value
+4 -2
View File
@@ -62,13 +62,15 @@ class FileBrowser(Plugin):
def view(self): def view(self):
path = getParam('path', '/')
try: try:
dirs = self.getDirectories(path = getParam('path', '/'), show_hidden = getParam('show_hidden', True)) dirs = self.getDirectories(path = path, show_hidden = getParam('show_hidden', True))
except: except:
dirs = [] dirs = []
return jsonified({ return jsonified({
'is_root': getParam('path', '/') == '/', 'is_root': path == '/' or not path,
'empty': len(dirs) == 0, 'empty': len(dirs) == 0,
'dirs': dirs, 'dirs': dirs,
}) })
+2
View File
@@ -8,6 +8,7 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
import os.path import os.path
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -46,6 +47,7 @@ class FileManager(Plugin):
try: try:
filedata = self.urlopen(url, **urlopen_kwargs) filedata = self.urlopen(url, **urlopen_kwargs)
except: except:
log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
return False return False
self.createFile(dest, filedata, binary = True) self.createFile(dest, filedata, binary = True)
+16 -13
View File
@@ -78,7 +78,7 @@ class LibraryPlugin(Plugin):
except: pass except: pass
if not info or len(info) == 0: if not info or len(info) == 0:
log.error('Could not update, no movie info to work with: %s' % identifier) log.error('Could not update, no movie info to work with: %s', identifier)
return False return False
# Main info # Main info
@@ -95,7 +95,7 @@ class LibraryPlugin(Plugin):
db.commit() db.commit()
titles = info.get('titles', []) titles = info.get('titles', [])
log.debug('Adding titles: %s' % titles) log.debug('Adding titles: %s', titles)
for title in titles: for title in titles:
if not title: if not title:
continue continue
@@ -117,26 +117,29 @@ class LibraryPlugin(Plugin):
continue continue
file_path = fireEvent('file.download', url = image, single = True) file_path = fireEvent('file.download', url = image, single = True)
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True) if file_path:
try: file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one() try:
library.files.append(file_obj) file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
db.commit() library.files.append(file_obj)
except: db.commit()
log.debug('Failed to attach to library: %s' % traceback.format_exc()) except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict) library_dict = library.to_dict(self.default_dict)
fireEvent('library.update_finish', data = library_dict)
#db.close()
return library_dict return library_dict
def updateReleaseDate(self, identifier): def updateReleaseDate(self, identifier):
db = get_session() db = get_session()
library = db.query(Library).filter_by(identifier = identifier).first() library = db.query(Library).filter_by(identifier = identifier).first()
dates = library.info.get('release_date')
if not library.info:
library_dict = self.update(identifier)
dates = library_dict.get('info', {}).get('release_dates')
else:
dates = library.info.get('release_date')
if dates and dates.get('expires', 0) < time.time(): if dates and dates.get('expires', 0) < time.time():
dates = fireEvent('movie.release_date', identifier = identifier, merge = True) dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
+6 -5
View File
@@ -1,5 +1,7 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam, getParams from couchpotato.core.helpers.request import jsonified, getParam, getParams
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -71,14 +73,14 @@ class Logging(Plugin):
return jsonified({ return jsonified({
'success': True, 'success': True,
'log': log, 'log': toUnicode(log),
'total': total, 'total': total,
}) })
def partial(self): def partial(self):
log_type = getParam('type', 'all') log_type = getParam('type', 'all')
total_lines = getParam('lines', 30) total_lines = tryInt(getParam('lines', 30))
log_lines = [] log_lines = []
@@ -92,13 +94,12 @@ class Logging(Plugin):
reversed_lines = [] reversed_lines = []
f = open(path, 'r') f = open(path, 'r')
reversed_lines = f.read().split('[0m\n') reversed_lines = toUnicode(f.read()).split('[0m\n')
reversed_lines.reverse() reversed_lines.reverse()
brk = False brk = False
for line in reversed_lines: for line in reversed_lines:
#print '%s ' % log_type in line.lower()
if log_type == 'all' or '%s ' % log_type.upper() in line: if log_type == 'all' or '%s ' % log_type.upper() in line:
log_lines.append(line) log_lines.append(line)
@@ -149,7 +150,7 @@ class Logging(Plugin):
except: except:
log.error(log_message) log.error(log_message)
except: except:
log.error('Couldn\'t log via API: %s' % params) log.error('Couldn\'t log via API: %s', params)
return jsonified({ return jsonified({
@@ -59,6 +59,7 @@
width: 14%; width: 14%;
color: lightgrey; color: lightgrey;
padding: 3px 0; padding: 3px 0;
font-size: 10px;
} }
.page.log .container .time:last-child { display: none; } .page.log .container .time:last-child { display: none; }
+11 -6
View File
@@ -45,7 +45,7 @@ Page.Log = new Class({
new Fx.Scroll(window, {'duration': 0}).toBottom(); new Fx.Scroll(window, {'duration': 0}).toBottom();
var nav = new Element('ul.nav').inject(self.log, 'top'); var nav = new Element('ul.nav').inject(self.log, 'top');
for (var i = 0; i < json.total; i++) { for (var i = 0; i <= json.total; i++) {
new Element('li', { new Element('li', {
'text': i+1, 'text': i+1,
'class': nr == i ? 'active': '', 'class': nr == i ? 'active': '',
@@ -78,11 +78,16 @@ Page.Log = new Class({
addColors: function(text){ addColors: function(text){
var self = this; var self = this;
text = text.replace(/\u001b\[31m/gi, '</span><span class="error">') text = text
text = text.replace(/\u001b\[36m/gi, '</span><span class="debug">') .replace(/&/g, '&amp;')
text = text.replace(/\u001b\[33m/gi, '</span><span class="debug">') .replace(/</g, '&lt;')
text = text.replace(/\u001b\[0m\n/gi, '</span><span class="time">') .replace(/>/g, '&gt;')
text = text.replace(/\u001b\[0m/gi, '</span><span>') .replace(/"/g, '&quot;')
.replace(/\u001b\[31m/gi, '</span><span class="error">')
.replace(/\u001b\[36m/gi, '</span><span class="debug">')
.replace(/\u001b\[33m/gi, '</span><span class="debug">')
.replace(/\u001b\[0m\n/gi, '</span><span class="time">')
.replace(/\u001b\[0m/gi, '</span><span>')
return '<span class="time">' + text + '</span>'; return '<span class="time">' + text + '</span>';
} }
+11 -10
View File
@@ -1,6 +1,6 @@
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import fireEvent, addEvent, fireEventAsync from couchpotato.core.event import fireEvent, addEvent, fireEventAsync
from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
@@ -25,13 +25,14 @@ class Manage(Plugin):
}) })
if not Env.get('dev'): if not Env.get('dev'):
addEvent('app.load', self.updateLibrary) def updateLibrary():
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def updateLibraryView(self): def updateLibraryView(self):
params = getParams() full = getParam('full', default = 1)
fireEventAsync('manage.update', full = True if full == '1' else False)
fireEventAsync('manage.update', full = params.get('full', True))
return jsonified({ return jsonified({
'success': True 'success': True
@@ -51,11 +52,11 @@ class Manage(Plugin):
if not os.path.isdir(directory): if not os.path.isdir(directory):
if len(directory) > 0: if len(directory) > 0:
log.error('Directory doesn\'t exist: %s' % directory) log.error('Directory doesn\'t exist: %s', directory)
continue continue
log.info('Updating manage library: %s' % directory) log.info('Updating manage library: %s', directory)
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update, single = True) identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
if identifiers: if identifiers:
added_identifiers.extend(identifiers) added_identifiers.extend(identifiers)
@@ -67,11 +68,11 @@ class Manage(Plugin):
if self.conf('cleanup') and full and not self.shuttingDown(): if self.conf('cleanup') and full and not self.shuttingDown():
# Get movies with done status # Get movies with done status
done_movies = fireEvent('movie.list', status = 'done', single = True) total_movies, done_movies = fireEvent('movie.list', status = 'done', single = True)
for done_movie in done_movies: for done_movie in done_movies:
if done_movie['library']['identifier'] not in added_identifiers: if done_movie['library']['identifier'] not in added_identifiers:
fireEvent('movie.delete', movie_id = done_movie['id']) fireEvent('movie.delete', movie_id = done_movie['id'], delete_from = 'all')
Env.prop('manage.last_update', time.time()) Env.prop('manage.last_update', time.time())
+63 -20
View File
@@ -130,6 +130,8 @@ class MoviePlugin(Plugin):
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \ .filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
.group_by(Movie.id) .group_by(Movie.id)
total_count = q.count()
filter_or = [] filter_or = []
if starts_with: if starts_with:
starts_with = toUnicode(starts_with.lower()) starts_with = toUnicode(starts_with.lower())
@@ -156,8 +158,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('library.titles')) \ .options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \ .options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \ .options(joinedload_all('status')) \
.options(joinedload_all('files')) \ .options(joinedload_all('files'))
if limit_offset: if limit_offset:
splt = [x.strip() for x in limit_offset.split(',')] splt = [x.strip() for x in limit_offset.split(',')]
@@ -165,7 +166,6 @@ class MoviePlugin(Plugin):
offset = 0 if len(splt) is 1 else splt[1] offset = 0 if len(splt) is 1 else splt[1]
q2 = q2.limit(limit).offset(offset) q2 = q2.limit(limit).offset(offset)
results = q2.all() results = q2.all()
movies = [] movies = []
for movie in results: for movie in results:
@@ -178,7 +178,7 @@ class MoviePlugin(Plugin):
movies.append(temp) movies.append(temp)
#db.close() #db.close()
return movies return (total_count, movies)
def availableChars(self, status = ['active']): def availableChars(self, status = ['active']):
@@ -214,11 +214,12 @@ class MoviePlugin(Plugin):
starts_with = params.get('starts_with', None) starts_with = params.get('starts_with', None)
search = params.get('search', None) search = params.get('search', None)
movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search) total_movies, movies = self.list(status = status, limit_offset = limit_offset, starts_with = starts_with, search = search)
return jsonified({ return jsonified({
'success': True, 'success': True,
'empty': len(movies) == 0, 'empty': len(movies) == 0,
'total': total_movies,
'movies': movies, 'movies': movies,
}) })
@@ -241,14 +242,16 @@ class MoviePlugin(Plugin):
for id in getParam('id').split(','): for id in getParam('id').split(','):
movie = db.query(Movie).filter_by(id = id).first() movie = db.query(Movie).filter_by(id = id).first()
# Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
if movie: if movie:
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True)
fireEventAsync('searcher.single', movie.to_dict(self.default_dict)) # Get current selected title
default_title = ''
for title in movie.library.titles:
if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title)
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
#db.close() #db.close()
return jsonified({ return jsonified({
@@ -277,6 +280,10 @@ class MoviePlugin(Plugin):
def add(self, params = {}, force_readd = True, search_after = True): def add(self, params = {}, force_readd = True, search_after = True):
if not params.get('identifier'):
log.error('Can\'t add movie without imdb identifier.')
return False
library = fireEvent('library.add', single = True, attrs = params, update_after = False) library = fireEvent('library.add', single = True, attrs = params, update_after = False)
# Status # Status
@@ -287,6 +294,7 @@ class MoviePlugin(Plugin):
db = get_session() db = get_session()
m = db.query(Movie).filter_by(library_id = library.get('id')).first() m = db.query(Movie).filter_by(library_id = library.get('id')).first()
added = True
do_search = False do_search = False
if not m: if not m:
m = Movie( m = Movie(
@@ -295,8 +303,14 @@ class MoviePlugin(Plugin):
status_id = status_active.get('id'), status_id = status_active.get('id'),
) )
db.add(m) db.add(m)
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', '')) db.commit()
do_search = True
onComplete = None
if search_after:
onComplete = self.createOnComplete(m.id)
fireEventAsync('library.update', params.get('identifier'), default_title = params.get('title', ''), on_complete = onComplete)
search_after = False
elif force_readd: elif force_readd:
# Clean snatched history # Clean snatched history
for release in m.releases: for release in m.releases:
@@ -305,10 +319,12 @@ class MoviePlugin(Plugin):
m.profile_id = params.get('profile_id', default_profile.get('id')) m.profile_id = params.get('profile_id', default_profile.get('id'))
else: else:
log.debug('Movie already exists, not updating: %s' % params) log.debug('Movie already exists, not updating: %s', params)
added = False
if force_readd: if force_readd:
m.status_id = status_active.get('id') m.status_id = status_active.get('id')
do_search = True
db.commit() db.commit()
@@ -321,8 +337,12 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
if (force_readd or do_search) and search_after: if do_search and search_after:
fireEventAsync('searcher.single', movie_dict) onComplete = self.createOnComplete(m.id)
onComplete()
if added:
fireEvent('notify.frontend', type = 'movie.added', data = movie_dict, message = 'Successfully added "%s" to your wanted list.' % params.get('title', ''))
#db.close() #db.close()
return movie_dict return movie_dict
@@ -336,7 +356,7 @@ class MoviePlugin(Plugin):
return jsonified({ return jsonified({
'success': True, 'success': True,
'added': True, 'added': True if movie_dict else False,
'movie': movie_dict, 'movie': movie_dict,
}) })
@@ -351,6 +371,9 @@ class MoviePlugin(Plugin):
for movie_id in ids: for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first() m = db.query(Movie).filter_by(id = movie_id).first()
if not m:
continue
m.profile_id = params.get('profile_id') m.profile_id = params.get('profile_id')
# Remove releases # Remove releases
@@ -369,7 +392,7 @@ class MoviePlugin(Plugin):
fireEvent('movie.restatus', m.id) fireEvent('movie.restatus', m.id)
movie_dict = m.to_dict(self.default_dict) movie_dict = m.to_dict(self.default_dict)
fireEventAsync('searcher.single', movie_dict) fireEventAsync('searcher.single', movie_dict, on_complete = self.createNotifyFront(movie_id))
#db.close() #db.close()
return jsonified({ return jsonified({
@@ -421,6 +444,7 @@ class MoviePlugin(Plugin):
db.commit() db.commit()
elif new_movie_status: elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True) new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None
movie.status_id = new_status.get('id') movie.status_id = new_status.get('id')
db.commit() db.commit()
else: else:
@@ -441,7 +465,7 @@ class MoviePlugin(Plugin):
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:
@@ -458,3 +482,22 @@ class MoviePlugin(Plugin):
#db.close() #db.close()
return True return True
def createOnComplete(self, movie_id):
def onComplete():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEventAsync('searcher.single', movie.to_dict(self.default_dict), on_complete = self.createNotifyFront(movie_id))
return onComplete
def createNotifyFront(self, movie_id):
def notifyFront():
db = get_session()
movie = db.query(Movie).filter_by(id = movie_id).first()
fireEvent('notify.frontend', type = 'movie.update.%s' % movie.id, data = movie.to_dict(self.default_dict))
return notifyFront
+57 -21
View File
@@ -5,10 +5,12 @@ var MovieList = new Class({
options: { options: {
navigation: true, navigation: true,
limit: 50, limit: 50,
menu: [] menu: [],
add_new: false
}, },
movies: [], movies: [],
movies_added: {},
letters: {}, letters: {},
filter: { filter: {
'startswith': null, 'startswith': null,
@@ -30,6 +32,17 @@ var MovieList = new Class({
}) })
); );
self.getMovies(); self.getMovies();
if(options.add_new)
App.addEvent('movie.added', self.movieAdded.bind(self))
},
movieAdded: function(notification){
var self = this;
window.scroll(0,0);
if(!self.movies_added[notification.data.id])
self.createMovie(notification.data, 'top');
}, },
create: function(){ create: function(){
@@ -59,7 +72,7 @@ var MovieList = new Class({
self.created = true; self.created = true;
}, },
addMovies: function(movies){ addMovies: function(movies, total){
var self = this; var self = this;
if(!self.created) self.create(); if(!self.created) self.create();
@@ -71,25 +84,41 @@ var MovieList = new Class({
} }
Object.each(movies, function(movie){ Object.each(movies, function(movie){
self.createMovie(movie);
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, {
'actions': actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list);
m.fireEvent('injected');
self.movies.include(m)
}); });
self.setCounter(total);
}, },
setCounter: function(count){
var self = this;
if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0));
},
createMovie: function(movie, inject_at){
var self = this;
// Attach proper actions
var a = self.options.actions,
status = Status.get(movie.status_id);
var actions = a[status.identifier.capitalize()] || a.Wanted || {};
var m = new Movie(self, {
'actions': actions,
'view': self.current_view,
'onSelect': self.calculateSelected.bind(self)
}, movie);
$(m).inject(self.movie_list, inject_at || 'bottom');
m.fireEvent('injected');
self.movies.include(m)
self.movies_added[movie.id] = true;
},
createNavigation: function(){ createNavigation: function(){
var self = this; var self = this;
@@ -100,6 +129,7 @@ var MovieList = new Class({
self.navigation = new Element('div.alph_nav').adopt( self.navigation = new Element('div.alph_nav').adopt(
self.navigation_actions = new Element('ul.inlay.actions.reversed'), self.navigation_actions = new Element('ul.inlay.actions.reversed'),
self.navigation_counter = new Element('span.counter[title=Total]'),
self.navigation_alpha = new Element('ul.numbers', { self.navigation_alpha = new Element('ul.numbers', {
'events': { 'events': {
'click:relay(li)': function(e, el){ 'click:relay(li)': function(e, el){
@@ -260,6 +290,7 @@ var MovieList = new Class({
'events': { 'events': {
'click': function(e){ 'click': function(e){
(e).preventDefault(); (e).preventDefault();
this.set('text', 'Deleting..')
Api.request('movie.delete', { Api.request('movie.delete', {
'data': { 'data': {
'id': ids.join(','), 'id': ids.join(','),
@@ -268,14 +299,19 @@ var MovieList = new Class({
'onSuccess': function(){ 'onSuccess': function(){
qObj.close(); qObj.close();
var erase_movies = [];
self.movies.each(function(movie){ self.movies.each(function(movie){
if (movie.isSelected()){ if (movie.isSelected()){
$(movie).destroy() $(movie).destroy()
self.movies.erase(movie) erase_movies.include(movie)
} }
}); });
self.calculateSelected() erase_movies.each(function(movie){
self.movies.erase(movie);
});
self.calculateSelected();
} }
}); });
@@ -419,7 +455,7 @@ var MovieList = new Class({
}, self.filter), }, self.filter),
'onComplete': function(json){ 'onComplete': function(json){
self.store(json.movies); self.store(json.movies);
self.addMovies(json.movies); self.addMovies(json.movies, json.total);
self.load_more.set('text', 'load more movies'); self.load_more.set('text', 'load more movies');
if(self.scrollspy) self.scrollspy.start(); if(self.scrollspy) self.scrollspy.start();
} }
@@ -24,6 +24,10 @@
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover { .movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
background: rgba(255,255,255,0.03); background: rgba(255,255,255,0.03);
} }
.movies .movie_container {
overflow: hidden;
}
.movies .data { .movies .data {
padding: 20px; padding: 20px;
@@ -32,7 +36,6 @@
position: relative; position: relative;
float: right; float: right;
border-radius: 0; border-radius: 0;
overflow: hidden;
transition: all 0.2s linear; transition: all 0.2s linear;
} }
.movies .list_view .data, .movies .mass_edit_view .data { .movies .list_view .data, .movies .mass_edit_view .data {
@@ -89,7 +92,7 @@
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 64%; width: auto;
} }
.movies .info .year { .movies .info .year {
@@ -152,7 +155,6 @@
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality { .movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
text-align: right; text-align: right;
float: right; float: right;
width: 30%;
} }
.movies .data .quality .available, .movies .data .quality .snatched { .movies .data .quality .available, .movies .data .quality .snatched {
@@ -200,6 +202,7 @@
.movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions { .movies .list_view .data:hover .actions, .movies .mass_edit_view .data:hover .actions {
margin: -34px 2px 0 0; margin: -34px 2px 0 0;
background: #4e5969; background: #4e5969;
position: relative;
} }
.movies .delete_container { .movies .delete_container {
@@ -310,6 +313,33 @@
padding-bottom: 4px; padding-bottom: 4px;
height: auto; height: auto;
} }
.movies .movie .trailer_container {
width: 100%;
background: #000;
text-align: center;
transition: all .6s cubic-bezier(0.9,0,0.1,1);
overflow: hidden;
}
.movies .movie .trailer_container.hide {
height: 0 !important;
}
.movies .movie .hide_trailer {
position: absolute;
top: 0;
left: 50%;
margin-left: -50px;
width: 100px;
text-align: center;
padding: 3px 10px;
background: #4e5969;
border-radius: 0 0 2px 2px;
transition: all .6s cubic-bezier(0.9,0,0.1,1) .2s;
}
.movies .movie .hide_trailer.hide {
top: -30px;
}
.movies .load_more { .movies .load_more {
display: block; display: block;
@@ -330,15 +360,17 @@
width: 1080px; width: 1080px;
margin: 0 -60px; margin: 0 -60px;
box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1); box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
background: #4e5969;
} }
.movies .alph_nav.float { .movies .alph_nav.float {
box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5); box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
border-radius: 0; border-radius: 0;
background: #4e5969;
} }
.movies .alph_nav ul.numbers, .movies .alph_nav ul.actions { .movies .alph_nav ul.numbers,
.movies .alph_nav .counter,
.movies .alph_nav ul.actions {
list-style: none; list-style: none;
padding: 0 0 1px; padding: 0 0 1px;
margin: 0; margin: 0;
@@ -346,10 +378,15 @@
user-select: none; user-select: none;
} }
.movies .alph_nav .counter {
width: 60px;
text-align: center;
}
.movies .alph_nav .numbers li, .movies .alph_nav .actions li { .movies .alph_nav .numbers li, .movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 22px; width: 20px;
height: 24px; height: 24px;
line-height: 26px; line-height: 26px;
text-align: center; text-align: center;
@@ -361,7 +398,6 @@
} }
.movies .alph_nav .numbers li:first-child { .movies .alph_nav .numbers li:first-child {
width: 43px; width: 43px;
margin-left: 7px;
} }
.movies .alph_nav li.available { .movies .alph_nav li.available {
color: rgba(255,255,255,0.8); color: rgba(255,255,255,0.8);
@@ -370,8 +406,8 @@
} }
.movies .alph_nav li.active.available, .movies .alph_nav li.available:hover { .movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
color: #fff; color: #fff;
font-size: 24px; font-size: 20px;
line-height: 24px; line-height: 20px;
} }
.movies .alph_nav input { .movies .alph_nav input {
+221 -41
View File
@@ -11,53 +11,124 @@ var Movie = new Class({
self.view = options.view || 'thumbs'; self.view = options.view || 'thumbs';
self.list = list; self.list = list;
self.el = new Element('div.movie.inlay');
self.profile = Quality.getProfile(data.profile_id) || {}; self.profile = Quality.getProfile(data.profile_id) || {};
self.parent(self, options); self.parent(self, options);
App.addEvent('movie.update.'+data.id, self.update.bind(self));
['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+data.id, function(notification){
if(notification.data)
self.busy(true)
});
})
},
busy: function(set_busy){
var self = this;
if(!set_busy){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, 400);
}
}
else if(!self.spinner) {
self.createMask();
self.spinner = createSpinner(self.mask);
self.positionMask();
self.mask.fade('in');
}
},
createMask: function(){
var self = this;
self.mask = new Element('div.mask', {
'styles': {
'z-index': '1'
}
}).inject(self.el, 'top').fade('hide');
self.positionMask();
},
positionMask: function(){
var self = this,
s = self.el.getSize()
return self.mask.setStyles({
'width': s.x,
'height': s.y
}).position({
'relativeTo': self.el
})
},
update: function(notification){
var self = this;
self.data = notification.data;
self.container.destroy();
self.profile = Quality.getProfile(self.data.profile_id) || {};
self.create();
self.busy(false);
}, },
create: function(){ create: function(){
var self = this; var self = this;
self.el = new Element('div.movie.inlay').adopt( self.el.adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', { self.container = new Element('div.movie_container').adopt(
'events': { self.select_checkbox = new Element('input[type=checkbox].inlay', {
'change': function(){ 'events': {
self.fireEvent('select') 'change': function(){
} self.fireEvent('select')
}
}),
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
} }
}) }
), }),
self.actions = new Element('div.actions') self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light', {
'tween': {
duration: 400,
transition: 'quint:in:out',
onComplete: self.fireEvent.bind(self, 'slideEnd')
}
}).adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),
self.description = new Element('div.description', {
'text': self.data.library.plot
}),
self.quality = new Element('div.quality', {
'events': {
'click': function(e){
var releases = self.el.getElement('.actions .releases');
if(releases)
releases.fireEvent('click', [e])
}
}
})
),
self.actions = new Element('div.actions')
)
) )
); );
@@ -150,7 +221,7 @@ var Movie = new Class({
self.el.removeEvents('outerClick') self.el.removeEvents('outerClick')
self.addEvent('slideEnd:once', function(){ self.addEvent('slideEnd:once', function(){
self.el.getElements('> :not(.data):not(.poster)').hide(); self.el.getElements('> :not(.data):not(.poster):not(.movie_container)').hide();
}); });
self.data_container.tween('right', -840, 0); self.data_container.tween('right', -840, 0);
@@ -178,6 +249,10 @@ var Movie = new Class({
isSelected: function(){ isSelected: function(){
return this.select_checkbox.get('checked'); return this.select_checkbox.get('checked');
},
toElement: function(){
return this.el;
} }
}); });
@@ -290,7 +365,7 @@ var ReleaseAction = new Class({
} catch(e){} } catch(e){}
new Element('div', { new Element('div', {
'class': 'item' 'class': 'item '+status.identifier
}).adopt( }).adopt(
new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}), new Element('span.name', {'text': self.get(release, 'name'), 'title': self.get(release, 'name')}),
new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}), new Element('span.status', {'text': status.identifier, 'class': 'release_status '+status.identifier}),
@@ -357,4 +432,109 @@ var ReleaseAction = new Class({
} }
});
var TrailerAction = new Class({
Extends: MovieAction,
id: null,
create: function(){
var self = this;
self.el = new Element('a.trailer', {
'title': 'Watch the trailer of ' + self.movie.getTitle(),
'events': {
'click': self.watch.bind(self)
}
});
},
watch: function(offset){
var self = this;
var data_url = 'http://gdata.youtube.com/feeds/videos?vq="{title}" {year} trailer&max-results=1&alt=json-in-script&orderby=relevance&sortorder=descending&format=5&fmt=18'
var url = data_url.substitute({
'title': self.movie.getTitle(),
'year': self.movie.get('year'),
'offset': offset || 1
}),
size = $(self.movie).getSize(),
height = (size.x/16)*9,
id = 'trailer-'+randomString();
self.player_container = new Element('div[id='+id+']');
self.container = new Element('div.hide.trailer_container')
.adopt(self.player_container)
.inject(self.movie.container, 'top');
self.container.setStyle('height', 0);
self.container.removeClass('hide');
self.close_button = new Element('a.hide.hide_trailer', {
'text': 'Hide trailer',
'events': {
'click': self.stop.bind(self)
}
}).inject(self.movie);
setTimeout(function(){
$(self.movie).setStyle('max-height', height);
self.container.setStyle('height', height);
}, 100)
new Request.JSONP({
'url': url,
'onComplete': function(json){
var video_url = json.feed.entry[0].id.$t.split('/'),
video_id = video_url[video_url.length-1];
self.player = new YT.Player(id, {
'height': height,
'width': size.x,
'videoId': video_id,
'playerVars': {
'autoplay': 1,
'showsearch': 0,
'wmode': 'transparent',
'iv_load_policy': 3
}
});
self.close_button.removeClass('hide');
var quality_set = false;
var change_quality = function(state){
if(!quality_set && (state.data == 1 || state.data || 2)){
try {
self.player.setPlaybackQuality('hd720');
quality_set = true;
}
catch(e){
}
}
}
self.player.addEventListener('onStateChange', change_quality);
}
}).send()
},
stop: function(){
var self = this;
self.player.stopVideo();
self.container.addClass('hide');
self.close_button.addClass('hide');
setTimeout(function(){
self.container.destroy()
self.close_button.destroy();
}, 1800)
}
}); });
@@ -221,7 +221,9 @@ Block.Search.Item = new Class({
} }
}).adopt( }).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', { self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0] 'src': info.images.poster[0],
'height': null,
'width': null
}) : null, }) : null,
new Element('div.info').adopt( new Element('div.info').adopt(
self.title = new Element('h2', { self.title = new Element('h2', {
@@ -299,11 +301,11 @@ Block.Search.Item = new Class({
'title': self.title_select.get('value'), 'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value') 'profile_id': self.profile_select.get('value')
}, },
'onComplete': function(){ 'onComplete': function(json){
self.options.empty(); self.options.empty();
self.options.adopt( self.options.adopt(
new Element('div.message', { new Element('div.message', {
'text': 'Movie succesfully added.' 'text': json.added ? 'Movie succesfully added.' : 'Movie didn\'t add properly. Check logs'
}) })
); );
}, },
@@ -332,8 +334,10 @@ Block.Search.Item = new Class({
self.options.adopt( self.options.adopt(
new Element('div').adopt( new Element('div').adopt(
self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', { self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0] 'src': self.info.images.poster[0],
'height': null,
'width': null
}) : null, }) : null,
self.info.in_wanted ? new Element('span.in_wanted', { self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
+2 -3
View File
@@ -135,8 +135,7 @@ class ProfilePlugin(Plugin):
success = True success = True
except Exception, e: except Exception, e:
message = 'Failed deleting Profile: %s' % e message = log.error('Failed deleting Profile: %s', e)
log.error(message)
#db.close() #db.close()
@@ -163,7 +162,7 @@ class ProfilePlugin(Plugin):
# Create default quality profile # Create default quality profile
order = -2 order = -2
for profile in profiles: for profile in profiles:
log.info('Creating default profile: %s' % profile.get('label')) log.info('Creating default profile: %s', profile.get('label'))
p = Profile( p = Profile(
label = toUnicode(profile.get('label')), label = toUnicode(profile.get('label')),
order = order order = order
+11 -11
View File
@@ -17,8 +17,8 @@ class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts']}, {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts']}, {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
@@ -26,7 +26,7 @@ class QualityPlugin(Plugin):
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']} {'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
] ]
pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr'] pre_releases = ['cam', 'ts', 'tc', 'r5', 'scr']
@@ -116,7 +116,7 @@ class QualityPlugin(Plugin):
quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first() quality = db.query(Quality).filter_by(identifier = q.get('identifier')).first()
if not quality: if not quality:
log.info('Creating quality: %s' % q.get('label')) log.info('Creating quality: %s', q.get('label'))
quality = Quality() quality = Quality()
db.add(quality) db.add(quality)
@@ -133,7 +133,7 @@ class QualityPlugin(Plugin):
).all() ).all()
if not profile: if not profile:
log.info('Creating profile: %s' % q.get('label')) log.info('Creating profile: %s', q.get('label'))
profile = Profile( profile = Profile(
core = True, core = True,
label = toUnicode(quality.label), label = toUnicode(quality.label),
@@ -170,20 +170,20 @@ class QualityPlugin(Plugin):
# Check tags # Check tags
if quality['identifier'] in words: if quality['identifier'] in words:
log.debug('Found via identifier "%s" in %s' % (quality['identifier'], cur_file)) log.debug('Found via identifier "%s" in %s', (quality['identifier'], cur_file))
return self.setCache(hash, quality) return self.setCache(hash, quality)
if list(set(quality.get('alternative', [])) & set(words)): if list(set(quality.get('alternative', [])) & set(words)):
log.debug('Found %s via alt %s in %s' % (quality['identifier'], quality.get('alternative'), cur_file)) log.debug('Found %s via alt %s in %s', (quality['identifier'], quality.get('alternative'), cur_file))
return self.setCache(hash, quality) return self.setCache(hash, quality)
for tag in quality.get('tags', []): for tag in quality.get('tags', []):
if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words): if isinstance(tag, tuple) and '.'.join(tag) in '.'.join(words):
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file)) log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality) return self.setCache(hash, quality)
if list(set(quality.get('tags', [])) & set(words)): if list(set(quality.get('tags', [])) & set(words)):
log.debug('Found %s via tag %s in %s' % (quality['identifier'], quality.get('tags'), cur_file)) log.debug('Found %s via tag %s in %s', (quality['identifier'], quality.get('tags'), cur_file))
return self.setCache(hash, quality) return self.setCache(hash, quality)
# Try again with loose testing # Try again with loose testing
@@ -191,7 +191,7 @@ class QualityPlugin(Plugin):
if quality: if quality:
return self.setCache(hash, quality) return self.setCache(hash, quality)
log.debug('Could not identify quality for: %s' % files) log.debug('Could not identify quality for: %s', files)
return None return None
def guessLoose(self, hash, extra): def guessLoose(self, hash, extra):
@@ -200,7 +200,7 @@ class QualityPlugin(Plugin):
# Last check on resolution only # Last check on resolution only
if quality.get('width', 480) == extra.get('resolution_width', 0): if quality.get('width', 480) == extra.get('resolution_width', 0):
log.debug('Found %s via resolution_width: %s == %s' % (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0))) log.debug('Found %s via resolution_width: %s == %s', (quality['identifier'], quality.get('width', 480), extra.get('resolution_width', 0)))
return self.setCache(hash, quality) return self.setCache(hash, quality)
if 480 <= extra.get('resolution_width', 0) <= 720: if 480 <= extra.get('resolution_width', 0) <= 720:
+3 -3
View File
@@ -79,7 +79,7 @@ class Release(Plugin):
rel.files.append(added_file) rel.files.append(added_file)
db.commit() db.commit()
except Exception, e: except Exception, e:
log.debug('Failed to attach "%s" to release: %s' % (cur_file, e)) log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
fireEvent('movie.restatus', movie.id) fireEvent('movie.restatus', movie.id)
@@ -143,7 +143,7 @@ class Release(Plugin):
item[info.identifier] = info.value item[info.identifier] = info.value
# Get matching provider # Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], single = True) provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
item['download'] = provider.download item['download'] = provider.download
fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({ fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
@@ -158,7 +158,7 @@ class Release(Plugin):
'success': True 'success': True
}) })
else: else:
log.error('Couldn\'t find release with id: %s' % id) log.error('Couldn\'t find release with id: %s', id)
#db.close() #db.close()
return jsonified({ return jsonified({
@@ -19,6 +19,9 @@ rename_options = {
'source': 'Source media (Bluray)', 'source': 'Source media (Bluray)',
'original': 'Original filename', 'original': 'Original filename',
'original_folder': 'Original foldername', 'original_folder': 'Original foldername',
'imdb_id': 'IMDB id (tt0123456)',
'cd': 'CD number (cd1)',
'cd_nr': 'Just the cd nr. (1)',
}, },
} }
+71 -51
View File
@@ -86,19 +86,13 @@ class Renamer(Plugin):
# Add _UNKNOWN_ if no library item is connected # Add _UNKNOWN_ if no library item is connected
if not group['library'] or not movie_title: if not group['library'] or not movie_title:
if group['dirname']: self.tagDir(group, 'unknown')
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % group['dirname']) continue
else: # Add it to filename
for file_type in group['files']:
for rename_me in group['files'][file_type]:
filename = os.path.basename(rename_me)
rename_files[rename_me] = rename_me.replace(filename, '_UNKNOWN_%s' % filename)
# Rename the files using the library data # Rename the files using the library data
else: else:
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True) group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
if not group['library']: if not group['library']:
log.error('Could not rename, no library item to work with: %s' % group_identifier) log.error('Could not rename, no library item to work with: %s', group_identifier)
continue continue
library = group['library'] library = group['library']
@@ -129,13 +123,14 @@ class Renamer(Plugin):
'source': group['meta_data']['source'], 'source': group['meta_data']['source'],
'resolution_width': group['meta_data'].get('resolution_width'), 'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'), 'resolution_height': group['meta_data'].get('resolution_height'),
'imdb_id': library['identifier'],
} }
for file_type in group['files']: for file_type in group['files']:
# Move nfo depending on settings # Move nfo depending on settings
if file_type is 'nfo' and not self.conf('rename_nfo'): if file_type is 'nfo' and not self.conf('rename_nfo'):
log.debug('Skipping, renaming of %s disabled' % file_type) log.debug('Skipping, renaming of %s disabled', file_type)
if self.conf('cleanup'): if self.conf('cleanup'):
for current_file in group['files'][file_type]: for current_file in group['files'][file_type]:
remove_files.append(current_file) remove_files.append(current_file)
@@ -194,7 +189,7 @@ class Renamer(Plugin):
break break
if not found: if not found:
log.error('Could not determine dvd structure for: %s' % current_file) log.error('Could not determine dvd structure for: %s', current_file)
# Do rename others # Do rename others
else: else:
@@ -208,7 +203,7 @@ class Renamer(Plugin):
if file_type is 'subtitle': if file_type is 'subtitle':
# rename subtitles with or without language # rename subtitles with or without language
#rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name) rename_files[current_file] = os.path.join(destination, final_folder_name, final_file_name)
sub_langs = group['subtitle_language'].get(current_file, []) sub_langs = group['subtitle_language'].get(current_file, [])
rename_extras = self.getRenameExtras( rename_extras = self.getRenameExtras(
@@ -263,13 +258,13 @@ class Renamer(Plugin):
# Mark movie "done" onces it found the quality with the finish check # Mark movie "done" onces it found the quality with the finish check
try: try:
if movie.status_id == active_status.get('id'): if movie.status_id == active_status.get('id') and movie.profile:
for profile_type in movie.profile.types: for profile_type in movie.profile.types:
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish: if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
movie.status_id = done_status.get('id') movie.status_id = done_status.get('id')
db.commit() db.commit()
except Exception, e: except Exception, e:
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc())) log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
# Go over current movie releases # Go over current movie releases
for release in movie.releases: for release in movie.releases:
@@ -279,30 +274,23 @@ class Renamer(Plugin):
# This is where CP removes older, lesser quality releases # This is where CP removes older, lesser quality releases
if release.quality.order > group['meta_data']['quality']['order']: if release.quality.order > group['meta_data']['quality']['order']:
log.info('Removing lesser quality %s for %s.' % (movie.library.titles[0].title, release.quality.label)) log.info('Removing lesser quality %s for %s.', (movie.library.titles[0].title, release.quality.label))
for current_file in release.files: for current_file in release.files:
remove_files.append(current_file) remove_files.append(current_file)
remove_releases.append(release) remove_releases.append(release)
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc # Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
elif release.quality.order is group['meta_data']['quality']['order']: elif release.quality.order is group['meta_data']['quality']['order']:
log.info('Same quality release already exists for %s, with quality %s. Assuming repack.' % (movie.library.titles[0].title, release.quality.label)) log.info('Same quality release already exists for %s, with quality %s. Assuming repack.', (movie.library.titles[0].title, release.quality.label))
for current_file in release.files: for current_file in release.files:
remove_files.append(current_file) remove_files.append(current_file)
remove_releases.append(release) remove_releases.append(release)
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan # Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
else: else:
log.info('Better quality release already exists for %s, with quality %s' % (movie.library.titles[0].title, release.quality.label)) log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
# Add _EXISTS_ to the parent dir # Add _EXISTS_ to the parent dir
if group['dirname']: self.tagDir(group, 'exists')
for rename_me in rename_files: # Don't rename anything in this group
rename_files[rename_me] = None
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_EXISTS_%s' % group['dirname'])
else: # Add it to filename
for rename_me in rename_files:
filename = os.path.basename(rename_me)
rename_files[rename_me] = rename_me.replace(filename, '_EXISTS_%s' % filename)
# Notify on rename fail # Notify on rename fail
download_message = 'Renaming of %s (%s) canceled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label) download_message = 'Renaming of %s (%s) canceled, exists in %s already.' % (movie.library.titles[0].title, group['meta_data']['quality']['label'], release.quality.label)
@@ -311,7 +299,6 @@ class Renamer(Plugin):
break break
elif release.status_id is snatched_status.get('id'): elif release.status_id is snatched_status.get('id'):
print release.quality.label, group['meta_data']['quality']['label']
if release.quality.id is group['meta_data']['quality']['id']: if release.quality.id is group['meta_data']['quality']['id']:
log.debug('Marking release as downloaded') log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id') release.status_id = downloaded_status.get('id')
@@ -323,14 +310,32 @@ class Renamer(Plugin):
for current_file in group['files']['leftover']: for current_file in group['files']['leftover']:
remove_files.append(current_file) remove_files.append(current_file)
elif not remove_leftovers: # Don't remove anything elif not remove_leftovers: # Don't remove anything
remove_files = [] break
# Remove files
for src in remove_files:
if isinstance(src, File):
src = src.path
if rename_files.get(src):
log.debug('Not removing file that will be renamed: %s', src)
continue
log.info('Removing "%s"', src)
try:
if os.path.isfile(src):
os.remove(src)
except:
log.error('Failed removing %s: %s', (src, traceback.format_exc()))
self.tagDir(group, 'failed_remove')
# Rename all files marked # Rename all files marked
group['renamed_files'] = [] group['renamed_files'] = []
for src in rename_files: for src in rename_files:
if rename_files[src]: if rename_files[src]:
dst = rename_files[src] dst = rename_files[src]
log.info('Renaming "%s" to "%s"' % (src, dst)) log.info('Renaming "%s" to "%s"', (src, dst))
# Create dir # Create dir
self.makeDir(os.path.dirname(dst)) self.makeDir(os.path.dirname(dst))
@@ -339,34 +344,23 @@ class Renamer(Plugin):
self.moveFile(src, dst) self.moveFile(src, dst)
group['renamed_files'].append(dst) group['renamed_files'].append(dst)
except: except:
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc())) log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
self.tagDir(group, 'failed_rename')
# Remove files
for src in remove_files:
if isinstance(src, File):
src = src.path
log.info('Removing "%s"' % src)
try:
os.remove(src)
except:
log.error('Failed removing %s: %s' % (src, traceback.format_exc()))
# Remove matching releases # Remove matching releases
for release in remove_releases: for release in remove_releases:
log.debug('Removing release %s' % release.identifier) log.debug('Removing release %s', release.identifier)
try: try:
db.delete(release) db.delete(release)
except: except:
log.error('Failed removing %s: %s' % (release.identifier, traceback.format_exc())) log.error('Failed removing %s: %s', (release.identifier, traceback.format_exc()))
if group['dirname'] and group['parentdir']: if group['dirname'] and group['parentdir']:
try: try:
log.info('Deleting folder: %s' % group['parentdir']) log.info('Deleting folder: %s', group['parentdir'])
self.deleteEmptyFolder(group['parentdir']) self.deleteEmptyFolder(group['parentdir'])
except: except:
log.error('Failed removing %s: %s' % (group['parentdir'], traceback.format_exc())) log.error('Failed removing %s: %s', (group['parentdir'], traceback.format_exc()))
# Search for trailers etc # Search for trailers etc
fireEventAsync('renamer.after', group) fireEventAsync('renamer.after', group)
@@ -398,17 +392,43 @@ class Renamer(Plugin):
return rename_files return rename_files
def tagDir(self, group, tag):
rename_files = {}
if group['dirname']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
else: # Add it to filename
for file_type in group['files']:
for rename_me in group['files'][file_type]:
filename = os.path.basename(rename_me)
rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
for src in rename_files:
if rename_files[src]:
dst = rename_files[src]
log.info('Renaming "%s" to "%s"', (src, dst))
# Create dir
self.makeDir(os.path.dirname(dst))
try:
self.moveFile(src, dst)
except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
raise
def moveFile(self, old, dest): def moveFile(self, old, dest):
try: try:
shutil.move(old, dest) shutil.move(old, dest)
try: try:
os.chmod(dest, Env.getPermission('folder')) os.chmod(dest, Env.getPermission('file'))
except: except:
log.error('Failed setting permissions for file: %s' % dest) log.error('Failed setting permissions for file: %s, %s', (dest, traceback.format_exc(1)))
except: except:
log.error("Couldn't move file '%s' to '%s': %s" % (old, dest, traceback.format_exc())) log.error('Couldn\'t move file "%s" to "%s": %s', (old, dest, traceback.format_exc()))
raise Exception raise Exception
return True return True
@@ -421,7 +441,7 @@ class Renamer(Plugin):
replaced = toUnicode(string) replaced = toUnicode(string)
for x, r in replacements.iteritems(): for x, r in replacements.iteritems():
if r is not None: if r is not None:
replaced = replaced.replace('<%s>' % toUnicode(x), toUnicode(r)) replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
else: else:
#If information is not available, we don't want the tag in the filename #If information is not available, we don't want the tag in the filename
replaced = replaced.replace('<' + x + '>', '') replaced = replaced.replace('<' + x + '>', '')
@@ -444,9 +464,9 @@ class Renamer(Plugin):
try: try:
os.rmdir(full_path) os.rmdir(full_path)
except: except:
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()))
try: try:
os.rmdir(folder) os.rmdir(folder)
except: except:
log.error('Couldn\'t remove empty directory %s: %s' % (folder, traceback.format_exc())) log.error('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
+146 -63
View File
@@ -1,11 +1,10 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import fireEvent, addEvent from couchpotato.core.event import fireEvent, addEvent
from couchpotato.core.helpers.encoding import toUnicode, simplifyString from couchpotato.core.helpers.encoding import toUnicode, simplifyString, ss
from couchpotato.core.helpers.variable import getExt, getImdb, tryInt from couchpotato.core.helpers.variable import getExt, getImdb, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File from couchpotato.core.settings.model import File, Movie
from couchpotato.environment import Env
from enzyme.exceptions import NoParserError, ParseError from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info from guessit import guess_movie_info
from subliminal.videos import Video from subliminal.videos import Video
@@ -24,10 +23,10 @@ class Scanner(Plugin):
'media': 314572800, # 300MB 'media': 314572800, # 300MB
'trailer': 1048576, # 1MB 'trailer': 1048576, # 1MB
} }
ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files ignored_in_path = ['_unpack', '_failed_', '_unknown_', '_exists_', '_failed_remove_', '_failed_rename_', '.appledouble', '.appledb', '.appledesktop', os.path.sep + '._', '.ds_store', 'cp.cpnfo'] #unpacking, smb-crap, hidden files
ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate'] ignore_names = ['extract', 'extracting', 'extracted', 'movie', 'movies', 'film', 'films', 'download', 'downloads', 'video_ts', 'audio_ts', 'bdmv', 'certificate']
extensions = { extensions = {
'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts'], 'movie': ['mkv', 'wmv', 'avi', 'mpg', 'mpeg', 'mp4', 'm2ts', 'iso', 'img', 'mdf', 'ts', 'm4v'],
'movie_extra': ['mds'], 'movie_extra': ['mds'],
'dvd': ['vts_*', 'vob'], 'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'], 'nfo': ['nfo', 'txt', 'tag'],
@@ -89,24 +88,28 @@ class Scanner(Plugin):
addEvent('scanner.partnumber', self.getPartNumber) addEvent('scanner.partnumber', self.getPartNumber)
def after_rename(group): def after_rename(group):
return self.scanFilesToLibrary(self, folder = group['destination_dir'], files = group['renamed_files']) return self.scanFilesToLibrary(folder = group['destination_dir'], files = group['renamed_files'])
addEvent('rename.after', after_rename) addEvent('renamer.after', after_rename)
def scanFilesToLibrary(self, folder = None, files = None): def scanFilesToLibrary(self, folder = None, files = None):
folder = os.path.normpath(folder)
groups = self.scan(folder = folder, files = files) groups = self.scan(folder = folder, files = files)
for group in groups.itervalues(): for group in groups.itervalues():
if group['library']: if group['library']:
fireEvent('release.add', group = group) fireEvent('release.add', group = group)
def scanFolderToLibrary(self, folder = None, newer_than = None, simple = True): def scanFolderToLibrary(self, folder = None, newer_than = 0, simple = True):
folder = os.path.normpath(folder)
if not os.path.isdir(folder): if not os.path.isdir(folder):
return return
groups = self.scan(folder = folder, simple = simple) groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
added_identifier = [] added_identifier = []
while True and not self.shuttingDown(): while True and not self.shuttingDown():
@@ -127,10 +130,12 @@ class Scanner(Plugin):
return added_identifier return added_identifier
def scan(self, folder = None, files = [], simple = False): def scan(self, folder = None, files = [], simple = False, newer_than = 0):
folder = ss(os.path.normpath(folder))
if not folder or not os.path.isdir(folder): if not folder or not os.path.isdir(folder):
log.error('Folder doesn\'t exists: %s' % folder) log.error('Folder doesn\'t exists: %s', folder)
return {} return {}
# Get movie "master" files # Get movie "master" files
@@ -141,19 +146,15 @@ class Scanner(Plugin):
if len(files) == 0: if len(files) == 0:
try: try:
files = [] files = []
for root, dirs, walk_files in os.walk(toUnicode(folder)): for root, dirs, walk_files in os.walk(folder):
for filename in walk_files: for filename in walk_files:
files.append(os.path.join(root, filename)) files.append(os.path.join(root, filename))
except: except:
try: log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
files = [] else:
folder = toUnicode(folder).encode(Env.get('encoding')) files = [ss(x) for x in files]
log.info('Trying to convert unicode to str path: %s, %s' % (folder, type(folder)))
for root, dirs, walk_files in os.walk(folder): db = get_session()
for filename in walk_files:
files.append(os.path.join(root, filename))
except:
log.error('Failed getting files from %s: %s' % (folder, traceback.format_exc()))
for file_path in files: for file_path in files:
@@ -207,7 +208,7 @@ class Scanner(Plugin):
for identifier, group in movie_files.iteritems(): for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier) if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
log.debug('Grouping files: %s' % identifier) log.debug('Grouping files: %s', identifier)
for file_path in group['unsorted_files']: for file_path in group['unsorted_files']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)] wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
@@ -231,19 +232,53 @@ class Scanner(Plugin):
# Group the files based on the identifier # Group the files based on the identifier
for identifier, group in movie_files.iteritems(): delete_identifiers = []
log.debug('Grouping files on identifier: %s' % identifier) for identifier, found_files in self.path_identifiers.iteritems():
log.debug('Grouping files on identifier: %s', identifier)
found_files = set(self.path_identifiers.get(identifier, [])) group = movie_files.get(identifier)
group['unsorted_files'].extend(found_files) if group:
group['unsorted_files'].extend(found_files)
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack # Remove the found files from the leftover stack
leftovers = leftovers - found_files leftovers = leftovers - set(found_files)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
# Cleaning up used
for identifier in delete_identifiers:
if self.path_identifiers.get(identifier):
del self.path_identifiers[identifier]
del delete_identifiers
# Group based on folder
delete_identifiers = []
for identifier, found_files in self.path_identifiers.iteritems():
log.debug('Grouping files on foldername: %s', identifier)
for ff in found_files:
new_identifier = self.createStringIdentifier(os.path.dirname(ff), folder)
group = movie_files.get(new_identifier)
if group:
group['unsorted_files'].extend([ff])
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack
leftovers = leftovers - set([ff])
# Break if CP wants to shut down
if self.shuttingDown():
break
# Cleaning up used
for identifier in delete_identifiers:
if self.path_identifiers.get(identifier):
del self.path_identifiers[identifier]
del delete_identifiers
# Determine file types # Determine file types
processed_movies = {} processed_movies = {}
@@ -256,13 +291,38 @@ class Scanner(Plugin):
# Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute # Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
file_too_new = False file_too_new = False
for cur_file in group['unsorted_files']: for cur_file in group['unsorted_files']:
file_time = os.path.getmtime(cur_file) if not os.path.isfile(cur_file):
if file_time > time.time() - 60: file_too_new = time.time()
file_too_new = tryInt(time.time() - file_time) break
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
for t in file_time:
if t > time.time() - 60:
file_too_new = tryInt(time.time() - t)
break
if file_too_new:
break break
if file_too_new: if file_too_new:
log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s' % (time.ctime(file_time), identifier)) log.info('Files seem to be still unpacking or just unpacked (created on %s), ignoring for now: %s', (time.ctime(file_time[0]), identifier))
# Delete the unsorted list
del group['unsorted_files']
continue
# Only process movies newer than x
if newer_than and newer_than > 0:
for cur_file in group['unsorted_files']:
file_time = [os.path.getmtime(cur_file), os.path.getctime(cur_file)]
if file_time[0] > time.time() or file_time[1] > time.time():
break
log.debug('None of the files have changed since %s for %s, skipping.', (time.ctime(newer_than), identifier))
# Delete the unsorted list
del group['unsorted_files']
continue continue
# Group extra (and easy) files first # Group extra (and easy) files first
@@ -284,10 +344,10 @@ class Scanner(Plugin):
group['files']['movie'] = self.getMediaFiles(group['unsorted_files']) group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
if len(group['files']['movie']) == 0: if len(group['files']['movie']) == 0:
log.error('Couldn\t find any movie files for %s' % identifier) log.error('Couldn\t find any movie files for %s', identifier)
continue continue
log.debug('Getting metadata for %s' % identifier) log.debug('Getting metadata for %s', identifier)
group['meta_data'] = self.getMetaData(group) group['meta_data'] = self.getMetaData(group)
# Subtitle meta # Subtitle meta
@@ -320,7 +380,11 @@ class Scanner(Plugin):
# Determine movie # Determine movie
group['library'] = self.determineMovie(group) group['library'] = self.determineMovie(group)
if not group['library']: if not group['library']:
log.error('Unable to determine movie: %s' % group['identifiers']) log.error('Unable to determine movie: %s', group['identifiers'])
else:
movie = db.query(Movie).filter_by(library_id = group['library']['id']).first()
group['movie_id'] = None if not movie else movie.id
processed_movies[identifier] = group processed_movies[identifier] = group
@@ -329,9 +393,9 @@ class Scanner(Plugin):
self.path_identifiers = {} self.path_identifiers = {}
if len(processed_movies) > 0: if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s' % (len(processed_movies), folder)) log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else: else:
log.debug('Found no movies in the folder %s' % (folder)) log.debug('Found no movies in the folder %s', (folder))
return processed_movies return processed_movies
def getMetaData(self, group): def getMetaData(self, group):
@@ -351,7 +415,7 @@ class Scanner(Plugin):
data['resolution_height'] = meta.get('resolution_height', 480) data['resolution_height'] = meta.get('resolution_height', 480)
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480) data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
except: except:
log.debug('Error parsing metadata: %s %s' % (cur_file, traceback.format_exc())) log.debug('Error parsing metadata: %s %s', (cur_file, traceback.format_exc()))
pass pass
if data.get('audio'): break if data.get('audio'): break
@@ -379,11 +443,11 @@ class Scanner(Plugin):
'resolution_height': tryInt(p.video[0].height), 'resolution_height': tryInt(p.video[0].height),
} }
except ParseError: except ParseError:
log.debug('Failed to parse meta for %s' % filename) log.debug('Failed to parse meta for %s', filename)
except NoParserError: except NoParserError:
log.debug('No parser found for %s' % filename) log.debug('No parser found for %s', filename)
except: except:
log.debug('Failed parsing %s' % filename) log.debug('Failed parsing %s', filename)
return {} return {}
@@ -396,7 +460,7 @@ class Scanner(Plugin):
scan_result = [] scan_result = []
for p in paths: for p in paths:
if not group['is_dvd']: if not group['is_dvd']:
video = Video.from_path(p) video = Video.from_path(toUnicode(p))
video_result = [(video, video.scan())] video_result = [(video, video.scan())]
scan_result.extend(video_result) scan_result.extend(video_result)
@@ -405,7 +469,7 @@ class Scanner(Plugin):
if s.language and s.path not in paths: if s.language and s.path not in paths:
detected_languages[s.path] = [s.language] detected_languages[s.path] = [s.language]
except: except:
log.debug('Failed parsing subtitle languages for %s: %s' % (paths, traceback.format_exc())) log.debug('Failed parsing subtitle languages for %s: %s', (paths, traceback.format_exc()))
# IDX # IDX
for extra in group['files']['subtitle_extra']: for extra in group['files']['subtitle_extra']:
@@ -421,7 +485,7 @@ class Scanner(Plugin):
if len(idx_langs) > 0 and os.path.isfile(sub_file): if len(idx_langs) > 0 and os.path.isfile(sub_file):
detected_languages[sub_file] = idx_langs detected_languages[sub_file] = idx_langs
except: except:
log.error('Failed parsing subtitle idx for %s: %s' % (extra, traceback.format_exc())) log.error('Failed parsing subtitle idx for %s: %s', (extra, traceback.format_exc()))
return detected_languages return detected_languages
@@ -434,7 +498,7 @@ class Scanner(Plugin):
for cur_file in files['movie']: for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file) imdb_id = self.getCPImdb(cur_file)
if imdb_id: if imdb_id:
log.debug('Found movie via CP tag: %s' % cur_file) log.debug('Found movie via CP tag: %s', cur_file)
break break
# Check and see if nfo contains the imdb-id # Check and see if nfo contains the imdb-id
@@ -443,11 +507,23 @@ class Scanner(Plugin):
for nfo_file in files['nfo']: for nfo_file in files['nfo']:
imdb_id = getImdb(nfo_file) imdb_id = getImdb(nfo_file)
if imdb_id: if imdb_id:
log.debug('Found movie via nfo file: %s' % nfo_file) log.debug('Found movie via nfo file: %s', nfo_file)
break break
except: except:
pass pass
# Check and see if filenames contains the imdb-id
if not imdb_id:
try:
for filetype in files:
for filetype_file in files[filetype]:
imdb_id = getImdb(filetype_file, check_inside = False)
if imdb_id:
log.debug('Found movie via imdb in filename: %s', nfo_file)
break
except:
pass
# Check if path is already in db # Check if path is already in db
if not imdb_id: if not imdb_id:
db = get_session() db = get_session()
@@ -455,7 +531,7 @@ class Scanner(Plugin):
f = db.query(File).filter_by(path = toUnicode(cur_file)).first() f = db.query(File).filter_by(path = toUnicode(cur_file)).first()
try: try:
imdb_id = f.library[0].identifier imdb_id = f.library[0].identifier
log.debug('Found movie via database: %s' % cur_file) log.debug('Found movie via database: %s', cur_file)
break break
except: except:
pass pass
@@ -469,7 +545,7 @@ class Scanner(Plugin):
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0]['imdb'] imdb_id = movie[0]['imdb']
if imdb_id: if imdb_id:
log.debug('Found movie via OpenSubtitleHash: %s' % cur_file) log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
break break
# Search based on identifiers # Search based on identifiers
@@ -486,17 +562,17 @@ class Scanner(Plugin):
if len(movie) > 0: if len(movie) > 0:
imdb_id = movie[0]['imdb'] imdb_id = movie[0]['imdb']
log.debug('Found movie via search: %s' % cur_file) log.debug('Found movie via search: %s', cur_file)
if imdb_id: break if imdb_id: break
else: else:
log.debug('Identifier to short to use for search: %s' % identifier) log.debug('Identifier to short to use for search: %s', identifier)
if imdb_id: if imdb_id:
return fireEvent('library.add', attrs = { return fireEvent('library.add', attrs = {
'identifier': imdb_id 'identifier': imdb_id
}, update_after = False, single = True) }, update_after = False, single = True)
log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.' % group['identifiers']) log.error('No imdb_id found for %s. Add a NFO file with IMDB id or add the year to the filename.', group['identifiers'])
return {} return {}
def getCPImdb(self, string): def getCPImdb(self, string):
@@ -585,17 +661,17 @@ class Scanner(Plugin):
# ignoredpaths # ignoredpaths
for i in self.ignored_in_path: for i in self.ignored_in_path:
if i in filename.lower(): if i in filename.lower():
log.debug('Ignored "%s" contains "%s".' % (filename, i)) log.debug('Ignored "%s" contains "%s".', (filename, i))
return False return False
# Sample file # Sample file
if self.isSampleFile(filename): if self.isSampleFile(filename):
log.debug('Is sample file "%s".' % filename) log.debug('Is sample file "%s".', filename)
return False return False
# Minimal size # Minimal size
if self.filesizeBetween(filename, self.minimal_filesize['media']): if self.filesizeBetween(filename, self.minimal_filesize['media']):
log.debug('File to small: %s' % filename) log.debug('File to small: %s', filename)
return False return False
# All is OK # All is OK
@@ -603,14 +679,14 @@ class Scanner(Plugin):
def isSampleFile(self, filename): def isSampleFile(self, filename):
is_sample = re.search('(^|[\W_])sample\d*[\W_]', filename.lower()) is_sample = re.search('(^|[\W_])sample\d*[\W_]', filename.lower())
if is_sample: log.debug('Is sample file: %s' % filename) if is_sample: log.debug('Is sample file: %s', filename)
return is_sample return is_sample
def filesizeBetween(self, file, min = 0, max = 100000): def filesizeBetween(self, file, min = 0, max = 100000):
try: try:
return (min * 1048576) < os.path.getsize(file) < (max * 1048576) return (min * 1048576) < os.path.getsize(file) < (max * 1048576)
except: except:
log.error('Couldn\'t get filesize of %s.' % file) log.error('Couldn\'t get filesize of %s.', file)
return False return False
@@ -629,7 +705,7 @@ class Scanner(Plugin):
identifier = self.removeCPTag(identifier) identifier = self.removeCPTag(identifier)
# groups, release tags, scenename cleaner, regex isn't correct # groups, release tags, scenename cleaner, regex isn't correct
identifier = re.sub(self.clean, '::', simplifyString(identifier)) identifier = re.sub(self.clean, '::', simplifyString(identifier)).strip(':')
# Year # Year
year = self.findYear(identifier) year = self.findYear(identifier)
@@ -703,26 +779,28 @@ class Scanner(Plugin):
def getReleaseNameYear(self, release_name, file_name = None): def getReleaseNameYear(self, release_name, file_name = None):
# Use guessit first # Use guessit first
guess = {}
if file_name: if file_name:
try: try:
guess = guess_movie_info(file_name) guess = guess_movie_info(toUnicode(file_name))
if guess.get('title') and guess.get('year'): if guess.get('title') and guess.get('year'):
return { guess = {
'name': guess.get('title'), 'name': guess.get('title'),
'year': guess.get('year'), 'year': guess.get('year'),
} }
except: except:
log.debug('Could not detect via guessit "%s": %s' % (file_name, traceback.format_exc())) log.debug('Could not detect via guessit "%s": %s', (file_name, traceback.format_exc()))
# Backup to simple # Backup to simple
cleaned = ' '.join(re.split('\W+', simplifyString(release_name))) cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
cleaned = re.sub(self.clean, ' ', cleaned) cleaned = re.sub(self.clean, ' ', cleaned)
year = self.findYear(cleaned) year = self.findYear(cleaned)
cp_guess = {}
if year: # Split name on year if year: # Split name on year
try: try:
movie_name = cleaned.split(year).pop(0).strip() movie_name = cleaned.split(year).pop(0).strip()
return { cp_guess = {
'name': movie_name, 'name': movie_name,
'year': int(year), 'year': int(year),
} }
@@ -731,11 +809,16 @@ class Scanner(Plugin):
else: # Split name on multiple spaces else: # Split name on multiple spaces
try: try:
movie_name = cleaned.split(' ').pop(0).strip() movie_name = cleaned.split(' ').pop(0).strip()
return { cp_guess = {
'name': movie_name, 'name': movie_name,
'year': int(year), 'year': int(year),
} }
except: except:
pass pass
return {} if cp_guess.get('year') == guess.get('year') and len(cp_guess.get('name', '')) > len(guess.get('name', '')):
return cp_guess
elif guess == {}:
return cp_guess
return guess
+14 -1
View File
@@ -4,7 +4,8 @@ from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \ from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
sizeScore, providerScore, duplicateScore sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
halfMultipartScore
log = CPLog(__name__) log = CPLog(__name__)
@@ -21,6 +22,7 @@ class Score(Plugin):
for movie_title in movie['library']['titles']: for movie_title in movie['library']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title'])) score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
score += sizeScore(nzb['size']) score += sizeScore(nzb['size'])
@@ -38,4 +40,15 @@ class Score(Plugin):
# Duplicates in name # Duplicates in name
score += duplicateScore(nzb['name'], getTitle(movie['library'])) score += duplicateScore(nzb['name'], getTitle(movie['library']))
# Partial ignored words
score += partialIgnoredScore(nzb['name'], getTitle(movie['library']))
# Ignore single downloads from multipart
score += halfMultipartScore(nzb['name'])
# Extra provider specific check
extra_score = nzb.get('extra_score')
if extra_score:
score += extra_score(nzb)
return score return score
+87 -4
View File
@@ -1,5 +1,6 @@
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt
from couchpotato.environment import Env from couchpotato.environment import Env
import re import re
@@ -16,11 +17,12 @@ name_scores = [
'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10', 'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10',
# Release groups # Release groups
'imbt:1', 'cocain:1', 'vomit:1', 'fico:1', 'arrow:1', 'pukka:1', 'prism:1', 'devise:1', 'esir:1', 'ctrlhd:1', 'imbt:1', 'cocain:1', 'vomit:1', 'fico:1', 'arrow:1', 'pukka:1', 'prism:1', 'devise:1', 'esir:1', 'ctrlhd:1',
'metis:1', 'diamond:1', 'wiki:1', 'cbgb:1', 'crossbow:1', 'sinners:1', 'amiable:1', 'refined:1', 'twizted:1', 'felony:1', 'hubris:1', 'machd:1', 'metis:10', 'diamond:10', 'wiki:10', 'cbgb:10', 'crossbow:1', 'sinners:10', 'amiable:10', 'refined:1', 'twizted:1', 'felony:1', 'hubris:1', 'machd:1',
# Extras # Extras
'extras:-40', 'trilogy:-40', 'extras:-40', 'trilogy:-40',
] ]
def nameScore(name, year): def nameScore(name, year):
''' Calculate score for words in the NZB name ''' ''' Calculate score for words in the NZB name '''
@@ -47,8 +49,8 @@ def nameScore(name, year):
return score return score
def nameRatioScore(nzb_name, movie_name):
def nameRatioScore(nzb_name, movie_name):
nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True)) nzb_words = re.split('\W+', fireEvent('scanner.create_file_identifier', nzb_name, single = True))
movie_words = re.split('\W+', simplifyString(movie_name)) movie_words = re.split('\W+', simplifyString(movie_name))
@@ -56,15 +58,68 @@ def nameRatioScore(nzb_name, movie_name):
return 10 - len(left_over) return 10 - len(left_over)
def namePositionScore(nzb_name, movie_name):
score = 0
nzb_words = re.split('\W+', simplifyString(nzb_name))
qualities = fireEvent('quality.all', single = True)
try:
nzb_name = re.search(r'([\'"])[^\1]*\1', nzb_name).group(0)
except:
pass
name_year = fireEvent('scanner.name_year', nzb_name, single = True)
# Give points for movies beginning with the correct name
name_split = simplifyString(nzb_name).split(simplifyString(movie_name))
if name_split[0].strip() == '':
score += 10
# If year is second in line, give more points
if len(name_split) > 1 and name_year:
after_name = name_split[1].strip()
if tryInt(after_name[:4]) == name_year.get('year', None):
score += 10
after_name = after_name[4:]
# Give -point to crap between year and quality
found_quality = None
for quality in qualities:
# Main in words
if quality['identifier'] in nzb_words:
found_quality = quality['identifier']
# Alt in words
for alt in quality['alternative']:
if alt in nzb_words:
found_quality = alt
break
if not found_quality:
return score - 20
allowed = []
for value in name_scores:
name, sc = value.split(':')
allowed.append(name)
inbetween = re.split('\W+', after_name.split(found_quality)[0].strip())
score -= (10 * len(set(inbetween) - set(allowed)))
return score
def sizeScore(size): def sizeScore(size):
return 0 if size else -20 return 0 if size else -20
def providerScore(provider): def providerScore(provider):
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']: if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
return 30 return 20
if provider in ['Newznab', 'Moovee', 'X264']: if provider in ['Newznab']:
return 10 return 10
return 0 return 0
@@ -79,3 +134,31 @@ def duplicateScore(nzb_name, movie_name):
duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1] duplicates = [x for i, x in enumerate(nzb_words) if nzb_words[i:].count(x) > 1]
return len(list(set(duplicates) - set(movie_words))) * -4 return len(list(set(duplicates) - set(movie_words))) * -4
def partialIgnoredScore(nzb_name, movie_name):
nzb_name = nzb_name.lower()
movie_name = movie_name.lower()
ignored_words = [x.strip().lower() for x in Env.setting('ignored_words', section = 'searcher').split(',')]
score = 0
for ignored_word in ignored_words:
if ignored_word in nzb_name and ignored_word not in movie_name:
score -= 5
return score
def halfMultipartScore(nzb_name):
wrong_found = 0
for nr in [1, 2, 3, 4, 5, 'i', 'ii', 'iii', 'iv', 'v', 'a', 'b', 'c', 'd', 'e']:
for wrong in ['cd', 'part', 'dis', 'disc', 'dvd']:
if '%s%s' % (wrong, nr) in nzb_name.lower():
wrong_found += 1
if wrong_found == 1:
return -30
return 0
@@ -29,7 +29,15 @@ config = [{
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': 'Ignored words', 'label': 'Ignored words',
'default': 'german, dutch, french, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub', 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub',
},
{
'name': 'preferred_method',
'label': 'First search',
'description': 'Which of the methods do you prefer',
'default': 'nzb',
'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
}, },
], ],
}, { }, {
+87 -38
View File
@@ -6,6 +6,7 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Movie, Release, ReleaseInfo from couchpotato.core.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
from inspect import ismethod, isfunction
from sqlalchemy.exc import InterfaceError from sqlalchemy.exc import InterfaceError
import datetime import datetime
import re import re
@@ -28,6 +29,9 @@ class Searcher(Plugin):
# Schedule cronjob # Schedule cronjob
fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) fireEvent('schedule.cron', 'searcher.all', self.all_movies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
addEvent('app.load', self.all_movies)
def all_movies(self): def all_movies(self):
if self.in_progress: if self.in_progress:
@@ -55,7 +59,7 @@ class Searcher(Plugin):
except IndexError: except IndexError:
fireEvent('library.update', movie_dict['library']['identifier'], force = True) fireEvent('library.update', movie_dict['library']['identifier'], force = True)
except: except:
log.error('Search failed for %s: %s' % (movie_dict['library']['identifier'], traceback.format_exc())) log.error('Search failed for %s: %s', (movie_dict['library']['identifier'], traceback.format_exc()))
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
@@ -77,36 +81,46 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True) available_status = fireEvent('status.get', 'available', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
default_title = getTitle(movie['library']) default_title = getTitle(movie['library'])
if not default_title: if not default_title:
return return
fireEvent('notify.frontend', type = 'searcher.started.%s' % movie['id'], data = True, message = 'Searching for "%s"' % default_title)
ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases): if not self.couldBeReleased(quality_type['quality']['identifier'], release_dates, pre_releases):
log.info('To early to search for %s, %s' % (quality_type['quality']['identifier'], default_title)) log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
continue continue
has_better_quality = 0 has_better_quality = 0
# See if beter quality is available # See if better quality is available
for release in movie['releases']: for release in movie['releases']:
if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] is not available_status.get('id'): if release['quality']['order'] <= quality_type['quality']['order'] and release['status_id'] not in [available_status.get('id'), ignored_status.get('id')]:
has_better_quality += 1 has_better_quality += 1
# Don't search for quality lower then already available. # Don't search for quality lower then already available.
if has_better_quality is 0: if has_better_quality is 0:
log.info('Search for %s in %s' % (default_title, quality_type['quality']['label'])) log.info('Search for %s in %s', (default_title, quality_type['quality']['label']))
quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True) quality = fireEvent('quality.single', identifier = quality_type['quality']['identifier'], single = True)
results = fireEvent('yarr.search', movie, quality, merge = True) results = fireEvent('yarr.search', movie, quality, merge = True)
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True) sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
if len(sorted_results) == 0: if len(sorted_results) == 0:
log.debug('Nothing found for %s in %s' % (default_title, quality_type['quality']['label'])) log.debug('Nothing found for %s in %s', (default_title, quality_type['quality']['label']))
download_preference = self.conf('preferred_method')
if download_preference != 'both':
sorted_results = sorted(sorted_results, key = lambda k: k['type'], reverse = (download_preference == 'torrent'))
# Check if movie isn't deleted while searching # Check if movie isn't deleted while searching
if not db.query(Movie).filter_by(id = movie.get('id')).first(): if not db.query(Movie).filter_by(id = movie.get('id')).first():
return break
# Add them to this movie releases list # Add them to this movie releases list
for nzb in sorted_results: for nzb in sorted_results:
@@ -137,31 +151,52 @@ class Searcher(Plugin):
rls.info.append(rls_info) rls.info.append(rls_info)
db.commit() db.commit()
except InterfaceError: except InterfaceError:
log.debug('Couldn\'t add %s to ReleaseInfo: %s' % (info, traceback.format_exc())) log.debug('Couldn\'t add %s to ReleaseInfo: %s', (info, traceback.format_exc()))
nzb['status_id'] = rls.status_id
for nzb in sorted_results: for nzb in sorted_results:
if nzb['status_id'] == ignored_status.get('id'):
log.info('Ignored: %s', nzb['name'])
continue
if nzb['score'] <= 0:
log.info('Ignored, score to low: %s', nzb['name'])
continue
downloaded = self.download(data = nzb, movie = movie) downloaded = self.download(data = nzb, movie = movie)
if downloaded: if downloaded is True:
return True ret = True
else: break
elif downloaded != 'try_next':
break break
else: else:
log.info('Better quality (%s) already available or snatched for %s' % (quality_type['quality']['label'], default_title)) log.info('Better quality (%s) already available or snatched for %s', (quality_type['quality']['label'], default_title))
fireEvent('movie.restatus', movie['id']) fireEvent('movie.restatus', movie['id'])
break break
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown() or ret:
break break
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close() #db.close()
return False return ret
def download(self, data, movie, manual = False): def download(self, data, movie, manual = False):
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status = fireEvent('status.get', 'snatched', single = True)
successful = fireEvent('download', data = data, movie = movie, manual = manual, single = True)
# Download movie to temp
filedata = None
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if filedata is 'try_next':
return filedata
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
if successful: if successful:
@@ -185,7 +220,7 @@ class Searcher(Plugin):
if movie['status_id'] == active_status.get('id'): if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']: for profile_type in movie['profile']['types']:
if profile_type['quality_id'] == rls.quality.id and profile_type['finish']: if profile_type['quality_id'] == rls.quality.id and profile_type['finish']:
log.info('Renamer disabled, marking movie as finished: %s' % log_movie) log.info('Renamer disabled, marking movie as finished: %s', log_movie)
# Mark release done # Mark release done
rls.status_id = done_status.get('id') rls.status_id = done_status.get('id')
@@ -196,7 +231,7 @@ class Searcher(Plugin):
mvie.status_id = done_status.get('id') mvie.status_id = done_status.get('id')
db.commit() db.commit()
except Exception, e: except Exception, e:
log.error('Failed marking movie finished: %s %s' % (e, traceback.format_exc())) log.error('Failed marking movie finished: %s %s', (e, traceback.format_exc()))
#db.close() #db.close()
return True return True
@@ -211,27 +246,29 @@ class Searcher(Plugin):
retention = Env.setting('retention', section = 'nzb') retention = Env.setting('retention', section = 'nzb')
if nzb.get('seeds') is None and retention < nzb.get('age', 0): if nzb.get('seeds') is None and retention < nzb.get('age', 0):
log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s' % (nzb['age'], retention, nzb['name'])) log.info('Wrong: Outside retention, age is %s, needs %s or lower: %s', (nzb['age'], retention, nzb['name']))
return False return False
movie_name = simplifyString(nzb['name']) movie_name = getTitle(movie['library'])
nzb_words = re.split('\W+', movie_name) movie_words = re.split('\W+', simplifyString(movie_name))
required_words = [x.strip() for x in self.conf('required_words').split(',')] nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name)
required_words = [x.strip().lower() for x in self.conf('required_words').lower().split(',')]
if self.conf('required_words') and not list(set(nzb_words) & set(required_words)): if self.conf('required_words') and not list(set(nzb_words) & set(required_words)):
log.info("NZB doesn't contain any of the required words.") log.info("Wrong: Required word missing: %s" % nzb['name'])
return False return False
ignored_words = [x.strip() for x in self.conf('ignored_words').split(',')] ignored_words = [x.strip().lower() for x in self.conf('ignored_words').split(',')]
blacklisted = list(set(nzb_words) & set(ignored_words)) blacklisted = list(set(nzb_words) & set(ignored_words))
if self.conf('ignored_words') and blacklisted: if self.conf('ignored_words') and blacklisted:
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
return False return False
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs'] pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
for p_tag in pron_tags: for p_tag in pron_tags:
if p_tag in movie_name: if p_tag in nzb_words and p_tag not in movie_words:
log.info('Wrong: %s, probably pr0n' % (nzb['name'])) log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False return False
#qualities = fireEvent('quality.all', single = True) #qualities = fireEvent('quality.all', single = True)
@@ -239,18 +276,28 @@ class Searcher(Plugin):
# Contains lower quality string # Contains lower quality string
if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single_category = single_category): if self.containsOtherQuality(nzb, movie_year = movie['library']['year'], preferred_quality = preferred_quality, single_category = single_category):
log.info('Wrong: %s, looking for %s' % (nzb['name'], quality['label'])) log.info('Wrong: %s, looking for %s', (nzb['name'], quality['label']))
return False return False
# File to small # File to small
if nzb['size'] and preferred_quality['size_min'] > nzb['size']: if nzb['size'] and preferred_quality['size_min'] > nzb['size']:
log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.' % (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min'])) log.info('"%s" is too small to be %s. %sMB instead of the minimal of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_min']))
return False return False
# File to large # File to large
if nzb['size'] and preferred_quality.get('size_max') < nzb['size']: if nzb['size'] and preferred_quality.get('size_max') < nzb['size']:
log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.' % (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max'])) log.info('"%s" is too large to be %s. %sMB instead of the maximum of %sMB.', (nzb['name'], preferred_quality['label'], nzb['size'], preferred_quality['size_max']))
return False
# Provider specific functions
get_more = nzb.get('get_more_info')
if get_more:
get_more(nzb)
extra_check = nzb.get('extra_check')
if extra_check and not extra_check(nzb):
return False return False
@@ -277,7 +324,7 @@ class Searcher(Plugin):
if self.checkNFO(nzb['name'], movie['library']['identifier']): if self.checkNFO(nzb['name'], movie['library']['identifier']):
return True return True
log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], getTitle(movie['library']), movie['library']['year'])) log.info("Wrong: %s, undetermined naming. Looking for '%s (%s)'" % (nzb['name'], movie_name, movie['library']['year']))
return False return False
def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False): def containsOtherQuality(self, nzb, movie_year = None, preferred_quality = {}, single_category = False):
@@ -379,17 +426,19 @@ class Searcher(Plugin):
if dates.get('theater') - 604800 < now: if dates.get('theater') - 604800 < now:
return True return True
else: else:
# 6 weeks after theater release # 12 weeks after theater release
if dates.get('theater') + 3628800 < now: if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
return True return True
# 6 weeks before dvd release if dates.get('dvd') > 0:
if dates.get('dvd') - 3628800 < now:
return True
# Dvd should be released # 3 weeks before dvd release
if dates.get('dvd') > 0 and dates.get('dvd') < now: if dates.get('dvd') - 1814400 < now:
return True return True
# Dvd should be released
if dates.get('dvd') < now:
return True
return False return False
+1 -1
View File
@@ -92,7 +92,7 @@ class StatusPlugin(Plugin):
for identifier, label in self.statuses.iteritems(): for identifier, label in self.statuses.iteritems():
s = db.query(Status).filter_by(identifier = identifier).first() s = db.query(Status).filter_by(identifier = identifier).first()
if not s: if not s:
log.info('Creating status: %s' % label) log.info('Creating status: %s', label)
s = Status( s = Status(
identifier = identifier, identifier = identifier,
label = toUnicode(label) label = toUnicode(label)
+21 -9
View File
@@ -1,10 +1,12 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType from couchpotato.core.settings.model import Library, FileType
from couchpotato.environment import Env from couchpotato.environment import Env
import subliminal import subliminal
import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -44,16 +46,26 @@ class Subtitle(Plugin):
if self.isDisabled(): return if self.isDisabled(): return
available_languages = sum(group['subtitle_language'].itervalues(), []) try:
downloaded = [] available_languages = sum(group['subtitle_language'].itervalues(), [])
for lang in self.getLanguages(): downloaded = []
if lang not in available_languages: files = [toUnicode(x) for x in group['files']['movie']]
download = subliminal.download_subtitles(group['files']['movie'], multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
downloaded.extend(download)
for d_sub in downloaded: for lang in self.getLanguages():
group['files']['subtitle'].add(d_sub.path) if lang not in available_languages:
group['subtitle_language'][d_sub.path] = [d_sub.language] download = subliminal.download_subtitles(files, multi = True, force = False, languages = [lang], services = self.services, cache_dir = Env.get('cache_dir'))
downloaded.extend(download)
for d_sub in downloaded:
group['files']['subtitle'].add(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language]
return True
except:
log.error('Failed searching for subtitle: %s', (traceback.format_exc()))
return False
def getLanguages(self): def getLanguages(self):
return [x.strip() for x in self.conf('languages').split(',')] return [x.strip() for x in self.conf('languages').split(',')]
+5 -2
View File
@@ -1,5 +1,5 @@
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import getExt from couchpotato.core.helpers.variable import getExt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
import os import os
@@ -17,13 +17,16 @@ class Trailer(Plugin):
if self.isDisabled() or len(group['files']['trailer']) > 0: return if self.isDisabled() or len(group['files']['trailer']) > 0: return
trailers = fireEvent('trailer.search', group = group, merge = True) trailers = fireEvent('trailer.search', group = group, merge = True)
if not trailers or trailers == []:
log.info('No trailers found for: %s', getTitle(group['library']))
return
for trailer in trailers.get(self.conf('quality'), []): for trailer in trailers.get(self.conf('quality'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer)) destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
if not os.path.isfile(destination): if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
else: else:
log.debug('Trailer already exists: %s' % destination) log.debug('Trailer already exists: %s', destination)
# Download first and break # Download first and break
break break
+1 -1
View File
@@ -73,7 +73,7 @@ class Userscript(Plugin):
'movie': fireEvent('userscript.get_movie_via_url', url = url, single = True) 'movie': fireEvent('userscript.get_movie_via_url', url = url, single = True)
} }
if not isDict(params['movie']): if not isDict(params['movie']):
log.error('Failed adding movie via url: %s' % url) log.error('Failed adding movie via url: %s', url)
params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info' params['error'] = params['movie'] if params['movie'] else 'Failed getting movie info'
return jsonified(params) return jsonified(params)
+15 -6
View File
@@ -20,7 +20,7 @@ class Automation(Plugin):
def _getMovies(self): def _getMovies(self):
if not self.canCheck(): if not self.canCheck():
log.debug('Just checked, skipping %s' % self.getName()) log.debug('Just checked, skipping %s', self.getName())
return [] return []
self.last_checked = time.time() self.last_checked = time.time()
@@ -35,19 +35,28 @@ class Automation(Plugin):
else: else:
return None return None
def isMinimal(self, identifier): def isMinimalMovie(self, movie):
if movie['rating'] and movie['rating'].get('imdb'):
movie = fireEvent('movie.info', identifier = identifier, merge = True) movie['votes'] = movie['rating']['imdb'][1]
movie['rating'] = movie['rating']['imdb'][0]
identifier = movie['imdb']
for minimal_type in ['year', 'rating', 'votes']: for minimal_type in ['year', 'rating', 'votes']:
type_value = movie.get(minimal_type, 0) type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type) type_min = self.getMinimal(minimal_type)
if type_value < type_min: if type_value < type_min:
log.info('%s to low for %s, need %s has %s' % (minimal_type, identifier, type_min, type_value)) log.info('%s too low for %s, need %s has %s', (minimal_type, identifier, type_min, type_value))
return False return False
return True return True
def getIMDBFromTitle(self, name, year = None):
result = fireEvent('movie.search', q = '%s %s' % (name, year), limit = 1, merge = True)
if len(result) > 0:
return result[0]
else:
return None
def getMinimal(self, min_type): def getMinimal(self, min_type):
return Env.setting(min_type, 'automation') return Env.setting(min_type, 'automation')
@@ -0,0 +1,23 @@
from .main import Bluray
def start():
return Bluray()
config = [{
'name': 'bluray',
'groups': [
{
'tab': 'automation',
'name': 'bluray_automation',
'label': 'Blu-ray.com',
'description': 'Imports movies from blu-ray.com. (uses minimal requirements)',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
],
},
],
}]
@@ -0,0 +1,62 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class Bluray(Automation, RSS):
interval = 1800
rss_url = 'http://www.blu-ray.com/rss/newreleasesfeed.xml'
def getIMDBids(self):
if self.isDisabled():
return
movies = []
RSSMovie = {'name': 'placeholder', 'year' : 'placeholder'}
RSSMovies = []
cache_key = 'bluray.%s' % md5(self.rss_url)
rss_data = self.getCache(cache_key, self.rss_url)
data = XMLTree.fromstring(rss_data)
if data:
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
RSSMovie['name'] = self.getTextElement(movie, "title").lower().split("blu-ray")[0].strip("(").rstrip()
RSSMovie['year'] = self.getTextElement(movie, "description").split("|")[1].strip("(").strip()
if not RSSMovie['name'].find("/") == -1: # make sure it is not a double movie release
continue
if int(RSSMovie['year']) < Env.setting('year', 'automation'): #do year filtering
continue
for test in RSSMovies:
if test.values() == RSSMovie.values(): # make sure we did not already include it...
break
else:
log.info('Release found: %s.' % RSSMovie)
RSSMovies.append(RSSMovie.copy())
if not RSSMovies:
log.info('No movies found.')
return
log.debug("Applying IMDB filter to found movies...")
for RSSMovie in RSSMovies:
imdb = self.getIMDBFromTitle(RSSMovie['name'] + ' ' + RSSMovie['year'])
if imdb:
if self.isMinimalMovie(imdb):
movies.append(imdb['imdb'])
return movies
@@ -10,7 +10,7 @@ config = [{
'tab': 'automation', 'tab': 'automation',
'name': 'imdb_automation', 'name': 'imdb_automation',
'label': 'IMDB', 'label': 'IMDB',
'description': 'From any <strong>public</strong> IMDB watchlists', 'description': 'From any <strong>public</strong> IMDB watchlists. Url should be the RSS link.',
'options': [ 'options': [
{ {
'name': 'automation_enabled', 'name': 'automation_enabled',
@@ -1,17 +1,17 @@
from couchpotato.core.helpers.variable import md5 from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5, getImdb
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation from couchpotato.core.providers.automation.base import Automation
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
import StringIO
import csv
import time import time
import traceback import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
class IMDB(Automation): class IMDB(Automation, RSS):
interval = 1800 interval = 1800
@@ -21,34 +21,45 @@ class IMDB(Automation):
return return
movies = [] movies = []
headers = {}
for csv_url in self.conf('automation_urls').split(','): enablers = self.conf('automation_urls_use').split(',')
prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
index = -1
for rss_url in self.conf('automation_urls').split(','):
index += 1
if not enablers[index]:
continue
elif 'rss.imdb' not in rss_url:
log.error('This isn\'t the correct url.: %s', rss_url)
continue
prop_name = 'automation.imdb.last_update.%s' % md5(rss_url)
last_update = float(Env.prop(prop_name, default = 0)) last_update = float(Env.prop(prop_name, default = 0))
last_movie_added = 0
try: try:
cache_key = 'imdb_csv.%s' % md5(csv_url) cache_key = 'imdb.rss.%s' % md5(rss_url)
csv_data = self.getCache(cache_key, csv_url)
csv_reader = csv.reader(StringIO.StringIO(csv_data))
if not headers:
nr = 0
for column in csv_reader.next():
headers[column] = nr
nr += 1
for row in csv_reader: rss_data = self.getCache(cache_key, rss_url)
created = int(time.mktime(parse(row[headers['created']]).timetuple())) data = XMLTree.fromstring(rss_data)
if created < last_update: rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
created = int(time.mktime(parse(self.getTextElement(movie, "pubDate")).timetuple()))
imdb = getImdb(self.getTextElement(movie, "link"))
if created > last_movie_added:
last_movie_added = created
if not imdb or created <= last_update:
continue continue
imdb = row[headers['const']] movies.append(imdb)
if imdb:
movies.append(imdb)
except: except:
log.error('Failed loading IMDB watchlist: %s %s' % (csv_url, traceback.format_exc())) log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
Env.prop(prop_name, time.time())
Env.prop(prop_name, last_movie_added)
return movies return movies
@@ -0,0 +1,23 @@
from .main import Kinepolis
def start():
return Kinepolis()
config = [{
'name': 'kinepolis',
'groups': [
{
'tab': 'automation',
'name': 'kinepolis_automation',
'label': 'Kinepolis',
'description': 'Imports movies from the current top 10 of kinepolis. (uses minimal requirements)',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
],
},
],
}]
@@ -0,0 +1,42 @@
from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import md5
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import datetime
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class Kinepolis(Automation, RSS):
interval = 1800
rss_url = 'http://kinepolis.be/nl/top10-box-office/feed'
def getIMDBids(self):
if self.isDisabled():
return
movies = []
RSSMovie = {'name': 'placeholder', 'year' : 'placeholder'}
cache_key = 'kinepolis.%s' % md5(self.rss_url)
rss_data = self.getCache(cache_key, self.rss_url)
data = XMLTree.fromstring(rss_data)
if data is not None:
rss_movies = self.getElements(data, 'channel/item')
for movie in rss_movies:
RSSMovie['name'] = self.getTextElement(movie, "title")
currentYear = datetime.datetime.now().strftime("%Y")
RSSMovie['year'] = currentYear
log.debug('Release found: %s.', RSSMovie)
imdb = self.getIMDBFromTitle(RSSMovie['name'], RSSMovie['year'])
if imdb:
movies.append(imdb['imdb'])
return movies
@@ -41,13 +41,19 @@ class Trakt(Automation):
def call(self, method_url): def call(self, method_url):
if self.conf('automation_password'): try:
headers = { if self.conf('automation_password'):
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1] headers = {
} 'Authorization': 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
else: }
headers = {} else:
headers = {}
cache_key = 'trakt.%s' % md5(method_url) cache_key = 'trakt.%s' % md5(method_url)
json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers) json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers)
return json.loads(json_string) if json_string:
return json.loads(json_string)
except:
log.error('Failed to get data from trakt, check your login.')
return []
+10 -4
View File
@@ -3,6 +3,9 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from urlparse import urlparse from urlparse import urlparse
from urllib import quote_plus
from couchpotato.core.helpers.encoding import simplifyString
import re import re
import time import time
@@ -31,7 +34,7 @@ class Provider(Plugin):
self.urlopen(test_url, 30) self.urlopen(test_url, 30)
self.is_available[host] = True self.is_available[host] = True
except: except:
log.error('"%s" unavailable, trying again in an 15 minutes.' % host) log.error('"%s" unavailable, trying again in an 15 minutes.', host)
self.is_available[host] = False self.is_available[host] = False
return self.is_available.get(host, False) return self.is_available.get(host, False)
@@ -62,8 +65,11 @@ class YarrProvider(Provider):
def search(self, movie, quality): def search(self, movie, quality):
return [] return []
def belongsTo(self, url, host = None): def belongsTo(self, url, provider = None, host = None):
try: try:
if provider and provider == self.getName():
return self
hostname = urlparse(url).hostname hostname = urlparse(url).hostname
if host and hostname in host: if host and hostname in host:
return self return self
@@ -73,7 +79,7 @@ class YarrProvider(Provider):
if hostname in download_url: if hostname in download_url:
return self return self
except: except:
log.debug('Url % s doesn\'t belong to %s' % (url, self.getName())) log.debug('Url % s doesn\'t belong to %s', (url, self.getName()))
return return
@@ -106,4 +112,4 @@ class YarrProvider(Provider):
return [self.cat_backup_id] return [self.cat_backup_id]
def found(self, new): def found(self, new):
log.info('Found: score(%(score)s) on %(provider)s: %(name)s' % new) log.info('Found: score(%(score)s) on %(provider)s: %(name)s', new)
+5 -5
View File
@@ -19,14 +19,14 @@ class MetaDataBase(Plugin):
def create(self, release): def create(self, release):
if self.isDisabled(): return if self.isDisabled(): return
log.info('Creating %s metadata.' % self.getName()) log.info('Creating %s metadata.', self.getName())
# Update library to get latest info # Update library to get latest info
try: try:
updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True) updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True)
release['library'] = mergeDicts(release['library'], updated_library) release['library'] = mergeDicts(release['library'], updated_library)
except: except:
log.error('Failed to update movie, before creating metadata: %s' % traceback.format_exc()) log.error('Failed to update movie, before creating metadata: %s', traceback.format_exc())
root_name = self.getRootName(release) root_name = self.getRootName(release)
meta_name = os.path.basename(root_name) meta_name = os.path.basename(root_name)
@@ -44,14 +44,14 @@ class MetaDataBase(Plugin):
# Get file content # Get file content
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release) content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release)
if content: if content:
log.debug('Creating %s file: %s' % (file_type, name)) log.debug('Creating %s file: %s', (file_type, name))
if os.path.isfile(content): if os.path.isfile(content):
shutil.copy2(content, name) shutil.copy2(content, name)
else: else:
self.createFile(name, content) self.createFile(name, content)
except: except:
log.error('Unable to create %s file: %s' % (file_type, traceback.format_exc())) log.error('Unable to create %s file: %s', (file_type, traceback.format_exc()))
def getRootName(self, data): def getRootName(self, data):
return return
@@ -75,7 +75,7 @@ class MetaDataBase(Plugin):
break break
for cur_file in data['library'].get('files', []): for cur_file in data['library'].get('files', []):
if cur_file.get('type_id') is file_type.get('id'): if cur_file.get('type_id') is file_type.get('id') and os.path.isfile(cur_file.get('path')):
return cur_file.get('path') return cur_file.get('path')
def getFanart(self, movie_info = {}, data = {}): def getFanart(self, movie_info = {}, data = {}):
@@ -77,7 +77,7 @@ class XBMC(MetaDataBase):
votes.text = str(v) votes.text = str(v)
break break
except: except:
log.debug('Failed adding rating info from %s: %s' % (rating_type, traceback.format_exc())) log.debug('Failed adding rating info from %s: %s', (rating_type, traceback.format_exc()))
# Genre # Genre
for genre in movie_info.get('genres', []): for genre in movie_info.get('genres', []):
@@ -1,10 +1,9 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.variable import mergeDicts from couchpotato.core.helpers.variable import mergeDicts, randomString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library from couchpotato.core.settings.model import Library
import time
import traceback import traceback
log = CPLog(__name__) log = CPLog(__name__)
@@ -23,11 +22,19 @@ class MovieResultModifier(Plugin):
# Combine on imdb id # Combine on imdb id
for item in results: for item in results:
imdb = item.get('imdb', 'random-%s' % time.time()) random_string = randomString()
imdb = item.get('imdb', random_string)
imdb = imdb if imdb else random_string
if not temp.get(imdb): if not temp.get(imdb):
temp[imdb] = self.getLibraryTags(imdb) temp[imdb] = self.getLibraryTags(imdb)
order.append(imdb) order.append(imdb)
if item.get('via_imdb'):
if order.index(imdb):
order.remove(imdb)
order.insert(0, imdb)
# Merge dicts # Merge dicts
temp[imdb] = mergeDicts(temp[imdb], item) temp[imdb] = mergeDicts(temp[imdb], item)
@@ -61,7 +68,7 @@ class MovieResultModifier(Plugin):
if release.status_id == done_status['id']: if release.status_id == done_status['id']:
temp['in_library'] = fireEvent('movie.get', movie.id, single = True) temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
except: except:
log.error('Tried getting more info on searched movies: %s' % traceback.format_exc()) log.error('Tried getting more info on searched movies: %s', traceback.format_exc())
#db.close() #db.close()
return temp return temp
@@ -32,10 +32,10 @@ class CouchPotatoApi(MovieProvider):
headers = {'X-CP-Version': fireEvent('app.version', single = True)} headers = {'X-CP-Version': fireEvent('app.version', single = True)}
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers) data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
dates = json.loads(data) dates = json.loads(data)
log.debug('Found ETA for %s: %s' % (identifier, dates)) log.debug('Found ETA for %s: %s', (identifier, dates))
return dates return dates
except Exception, e: except Exception, e:
log.error('Error getting ETA for %s: %s' % (identifier, e)) log.error('Error getting ETA for %s: %s', (identifier, e))
return {} return {}
@@ -43,9 +43,9 @@ class CouchPotatoApi(MovieProvider):
try: try:
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/') data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
suggestions = json.loads(data) suggestions = json.loads(data)
log.info('Found Suggestions for %s' % (suggestions)) log.info('Found Suggestions for %s', (suggestions))
except Exception, e: except Exception, e:
log.error('Error getting suggestions for %s: %s' % (movies, e)) log.error('Error getting suggestions for %s: %s', (movies, e))
return suggestions return suggestions
@@ -21,6 +21,7 @@ class IMDBAPI(MovieProvider):
def __init__(self): def __init__(self):
addEvent('movie.search', self.search) addEvent('movie.search', self.search)
addEvent('movie.searchimdb', self.search)
addEvent('movie.info', self.getInfo) addEvent('movie.info', self.getInfo)
def search(self, q, limit = 12): def search(self, q, limit = 12):
@@ -36,7 +37,7 @@ class IMDBAPI(MovieProvider):
if cached: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
if result.get('titles') and len(result.get('titles')) > 0: if result.get('titles') and len(result.get('titles')) > 0:
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')')
return [result] return [result]
return [] return []
@@ -54,7 +55,7 @@ class IMDBAPI(MovieProvider):
if cached: if cached:
result = self.parseMovie(cached) result = self.parseMovie(cached)
if result.get('titles') and len(result.get('titles')) > 0: if result.get('titles') and len(result.get('titles')) > 0:
log.info('Found: %s' % result['titles'][0] + ' (' + str(result['year']) + ')') log.info('Found: %s', result['titles'][0] + ' (' + str(result['year']) + ')')
return result return result
return {} return {}
@@ -82,13 +83,14 @@ class IMDBAPI(MovieProvider):
year = tryInt(movie.get('Year', '')) year = tryInt(movie.get('Year', ''))
movie_data = { movie_data = {
'via_imdb': True,
'titles': [movie.get('Title')] if movie.get('Title') else [], 'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title', ''), 'original_title': movie.get('Title', ''),
'images': { 'images': {
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [], 'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
}, },
'rating': { 'rating': {
'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', ''))), 'imdb': (tryFloat(movie.get('imdbRating', 0)), tryInt(movie.get('imdbVotes', '').replace(',', ''))),
#'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', 0))), #'rotten': (tryFloat(movie.get('tomatoRating', 0)), tryInt(movie.get('tomatoReviews', 0))),
}, },
'imdb': str(movie.get('imdbID', '')), 'imdb': str(movie.get('imdbID', '')),
@@ -102,17 +104,17 @@ class IMDBAPI(MovieProvider):
'actors': movie.get('Actors', '').split(','), 'actors': movie.get('Actors', '').split(','),
} }
except: except:
log.error('Failed parsing IMDB API json: %s' % traceback.format_exc()) log.error('Failed parsing IMDB API json: %s', traceback.format_exc())
return movie_data return movie_data
def runtimeToMinutes(self, runtime_str): def runtimeToMinutes(self, runtime_str):
runtime = 0 runtime = 0
regex = '(\d*.?\d+).(hr|hrs|mins|min)+' regex = '(\d*.?\d+).(h|hr|hrs|mins|min)+'
matches = re.findall(regex, runtime_str) matches = re.findall(regex, runtime_str)
for match in matches: for match in matches:
nr, size = match nr, size = match
runtime += tryInt(nr) * (60 if 'hr' in str(size) else 1) runtime += tryInt(nr) * (60 if 'h' is str(size)[0] else 1)
return runtime return runtime
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from libs.themoviedb import tmdb from libs.themoviedb import tmdb
import re
log = CPLog(__name__) log = CPLog(__name__)
@@ -29,7 +28,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key) results = self.getCache(cache_key)
if not results: if not results:
log.debug('Searching for movie by hash: %s' % file) log.debug('Searching for movie by hash: %s', file)
try: try:
raw = tmdb.searchByHashingFile(file) raw = tmdb.searchByHashingFile(file)
@@ -37,15 +36,15 @@ class TheMovieDb(MovieProvider):
if raw: if raw:
try: try:
results = self.parseMovie(raw) results = self.parseMovie(raw)
log.info('Found: %s' % results['titles'][0] + ' (' + str(results['year']) + ')') log.info('Found: %s', results['titles'][0] + ' (' + str(results['year']) + ')')
self.setCache(cache_key, results) self.setCache(cache_key, results)
return results return results
except SyntaxError, e: except SyntaxError, e:
log.error('Failed to parse XML response: %s' % e) log.error('Failed to parse XML response: %s', e)
return False return False
except: except:
log.debug('No movies known by hash for: %s' % file) log.debug('No movies known by hash for: %s', file)
pass pass
return results return results
@@ -61,7 +60,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key) results = self.getCache(cache_key)
if not results: if not results:
log.debug('Searching for movie: %s' % q) log.debug('Searching for movie: %s', q)
raw = tmdb.search(search_string) raw = tmdb.search(search_string)
results = [] results = []
@@ -76,18 +75,21 @@ class TheMovieDb(MovieProvider):
if nr == limit: if nr == limit:
break break
log.info('Found: %s' % [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results]) log.info('Found: %s', [result['titles'][0] + ' (' + str(result['year']) + ')' for result in results])
self.setCache(cache_key, results) self.setCache(cache_key, results)
return results return results
except SyntaxError, e: except SyntaxError, e:
log.error('Failed to parse XML response: %s' % e) log.error('Failed to parse XML response: %s', e)
return False return False
return results return results
def getInfo(self, identifier = None): def getInfo(self, identifier = None):
if not identifier:
return {}
cache_key = 'tmdb.cache.%s' % identifier cache_key = 'tmdb.cache.%s' % identifier
result = self.getCache(cache_key) result = self.getCache(cache_key)
@@ -96,7 +98,7 @@ class TheMovieDb(MovieProvider):
movie = None movie = None
try: try:
log.debug('Getting info: %s' % cache_key) log.debug('Getting info: %s', cache_key)
movie = tmdb.imdbLookup(id = identifier) movie = tmdb.imdbLookup(id = identifier)
except: except:
pass pass
@@ -117,7 +119,7 @@ class TheMovieDb(MovieProvider):
movie = None movie = None
try: try:
log.debug('Getting info: %s' % cache_key) log.debug('Getting info: %s', cache_key)
movie = tmdb.getMovieInfo(id = id) movie = tmdb.getMovieInfo(id = id)
except: except:
pass pass
@@ -148,7 +150,8 @@ class TheMovieDb(MovieProvider):
year = None year = None
movie_data = { movie_data = {
'id': int(movie.get('id', 0)), 'via_tmdb': True,
'tmdb_id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))], 'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'), 'original_title': movie.get('original_name'),
'images': { 'images': {
@@ -1,66 +0,0 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
from dateutil.parser import parse
import re
import time
log = CPLog(__name__)
class Moovee(NZBProvider):
urls = {
'download': 'http://85.214.105.230/get_nzb.php?id=%s&section=moovee',
'search': 'http://abmoovee.allfilled.com/search.php?q=%s&Search=Search',
}
regex = '<td class="cell_reqid">(?P<reqid>.*?)</td>.+?<td class="cell_request">(?P<title>.*?)</td>.+?<td class="cell_statuschange">(?P<age>.*?)</td>'
http_time_between_calls = 2 # Seconds
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']) or quality.get('hd', False):
return results
q = '%s %s' % (getTitle(movie['library']), quality.get('identifier'))
url = self.urls['search'] % tryUrlencode(q)
cache_key = 'moovee.%s' % q
data = self.getCache(cache_key, url)
if data:
match = re.compile(self.regex, re.DOTALL).finditer(data)
for nzb in match:
new = {
'id': nzb.group('reqid'),
'name': nzb.group('title'),
'type': 'nzb',
'provider': self.getName(),
'age': self.calculateAge(time.mktime(parse(nzb.group('age')).timetuple())),
'size': None,
'url': self.urls['download'] % (nzb.group('reqid')),
'detail_url': '',
'description': '',
'check_nzb': False,
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
def belongsTo(self, url, host = None):
match = re.match('http://85\.214\.105\.230/get_nzb\.php\?id=[0-9]*&section=moovee', url)
if match:
return self
return
@@ -1,6 +1,7 @@
from BeautifulSoup import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.variable import tryInt, getTitle from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
@@ -22,11 +23,13 @@ class Mysterbin(NZBProvider):
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']): if self.isDisabled():
return results return results
q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier')) q = '"%s" %s %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
for ignored in Env.setting('ignored_words', 'searcher').split(','): for ignored in Env.setting('ignored_words', 'searcher').split(','):
if len(q) + len(ignored.strip()) > 126:
break
q = '%s -%s' % (q, ignored.strip()) q = '%s -%s' % (q, ignored.strip())
params = { params = {
@@ -39,28 +42,28 @@ class Mysterbin(NZBProvider):
'nopasswd': 'on', 'nopasswd': 'on',
} }
cache_key = 'mysterbin.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) cache_key = 'mysterbin.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params)) data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data: if data:
try: try:
html = BeautifulSoup(data) html = BeautifulSoup(data)
resultable = html.find('table', attrs = {'class':'t'}) resultable = html.find('table', attrs = {'class':'t'})
for result in resultable.findAll('tr'): for result in resultable.find_all('tr'):
try: try:
myster_id = result.find('input', attrs = {'class': 'check4nzb'})['value'] myster_id = result.find('input', attrs = {'class': 'check4nzb'})['value']
# Age # Age
age = '' age = ''
for temp in result.find('td', attrs = {'class': 'cdetail'}).findAll(text = True): for temp in result.find('td', attrs = {'class': 'cdetail'}).find_all(text = True):
if 'days' in temp: if 'days' in temp:
age = tryInt(temp.split(' ')[0]) age = tryInt(temp.split(' ')[0])
break break
# size # size
size = None size = None
for temp in result.find('div', attrs = {'class': 'cdetail'}).findAll(text = True): for temp in result.find('div', attrs = {'class': 'cdetail'}).find_all(text = True):
if 'gb' in temp.lower() or 'mb' in temp.lower() or 'kb' in temp.lower(): if 'gb' in temp.lower() or 'mb' in temp.lower() or 'kb' in temp.lower():
size = self.parseSize(temp) size = self.parseSize(temp)
break break
@@ -71,13 +74,14 @@ class Mysterbin(NZBProvider):
new = { new = {
'id': myster_id, 'id': myster_id,
'name': ''.join(result.find('span', attrs = {'class': 'cname'}).findAll(text = True)), 'name': ''.join(result.find('span', attrs = {'class': 'cname'}).find_all(text = True)),
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(), 'provider': self.getName(),
'age': age, 'age': age,
'size': size, 'size': size,
'url': self.urls['download'] % myster_id, 'url': self.urls['download'] % myster_id,
'description': description, 'description': description,
'download': self.download,
'check_nzb': False, 'check_nzb': False,
} }
@@ -39,7 +39,7 @@ class Newzbin(NZBProvider, RSS):
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']): if self.isDisabled():
return results return results
format_id = self.getFormatId(type) format_id = self.getFormatId(type)
@@ -57,11 +57,12 @@ class Newzbin(NZBProvider, RSS):
'category': '6', 'category': '6',
'ps_rb_video_format': str(cat_id), 'ps_rb_video_format': str(cat_id),
'ps_rb_source': str(format_id), 'ps_rb_source': str(format_id),
'u_post_larger_than': quality.get('size_min'),
'u_post_smaller_than': quality.get('size_max'),
}) })
url = "%s?%s" % (self.urls['search'], arguments) url = "%s?%s" % (self.urls['search'], arguments)
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id))) cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
single_cat = True
data = self.getCache(cache_key) data = self.getCache(cache_key)
if not data: if not data:
@@ -81,7 +82,7 @@ class Newzbin(NZBProvider, RSS):
data = XMLTree.fromstring(data) data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item') nzbs = self.getElements(data, 'channel/item')
except Exception, e: except Exception, e:
log.debug('%s, %s' % (self.getName(), e)) log.debug('%s, %s', (self.getName(), e))
return results return results
for nzb in nzbs: for nzb in nzbs:
@@ -115,12 +116,12 @@ class Newzbin(NZBProvider, RSS):
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': False, 'check_nzb': False,
} }
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True) imdb_results = True, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new) results.append(new)
self.found(new) self.found(new)
@@ -132,7 +133,7 @@ class Newzbin(NZBProvider, RSS):
def download(self, url = '', nzb_id = ''): def download(self, url = '', nzb_id = ''):
try: try:
log.info('Download nzb from newzbin, report id: %s ' % nzb_id) log.info('Download nzb from newzbin, report id: %s ', nzb_id)
return self.urlopen(self.urls['download'], params = { return self.urlopen(self.urls['download'], params = {
'username' : self.conf('username'), 'username' : self.conf('username'),
@@ -140,7 +141,7 @@ class Newzbin(NZBProvider, RSS):
'reportid' : nzb_id 'reportid' : nzb_id
}, show_error = False) }, show_error = False)
except Exception, e: except Exception, e:
log.error('Failed downloading from newzbin, check credit: %s' % e) log.error('Failed downloading from newzbin, check credit: %s', e)
return False return False
def getFormatId(self, format): def getFormatId(self, format):
+38 -12
View File
@@ -6,7 +6,10 @@ from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
from couchpotato.environment import Env from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
from urllib2 import HTTPError
from urlparse import urlparse
import time import time
import traceback
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -20,6 +23,8 @@ class Newznab(NZBProvider, RSS):
'search': 'movie', 'search': 'movie',
} }
limits_reached = {}
cat_ids = [ cat_ids = [
([2010], ['dvdr']), ([2010], ['dvdr']),
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']), ([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
@@ -46,7 +51,7 @@ class Newznab(NZBProvider, RSS):
def singleFeed(self, host): def singleFeed(self, host):
results = [] results = []
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])): if self.isDisabled(host):
return results return results
arguments = tryUrlencode({ arguments = tryUrlencode({
@@ -78,7 +83,7 @@ class Newznab(NZBProvider, RSS):
def singleSearch(self, host, movie, quality): def singleSearch(self, host, movie, quality):
results = [] results = []
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])): if self.isDisabled(host):
return results return results
cat_id = self.getCatId(quality['identifier']) cat_id = self.getCatId(quality['identifier'])
@@ -107,7 +112,7 @@ class Newznab(NZBProvider, RSS):
data = XMLTree.fromstring(data) data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item') nzbs = self.getElements(data, 'channel/item')
except Exception, e: except Exception, e:
log.debug('%s, %s' % (self.getName(), e)) log.debug('%s, %s', (self.getName(), e))
return results return results
results = [] results = []
@@ -121,8 +126,8 @@ class Newznab(NZBProvider, RSS):
elif item.attrib.get('name') == 'usenetdate': elif item.attrib.get('name') == 'usenetdate':
date = item.attrib.get('value') date = item.attrib.get('value')
if date is '': log.debug('Date not parsed properly or not available for %s: %s' % (host['host'], self.getTextElement(nzb, "title"))) if date is '': log.debug('Date not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title")))
if size is 0: log.debug('Size not parsed properly or not available for %s: %s' % (host['host'], self.getTextElement(nzb, "title"))) if size is 0: log.debug('Size not parsed properly or not available for %s: %s', (host['host'], self.getTextElement(nzb, "title")))
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop() id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = { new = {
@@ -139,13 +144,12 @@ class Newznab(NZBProvider, RSS):
} }
if not for_feed: if not for_feed:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True) imdb_results = True, single_category = single_cat, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new) results.append(new)
self.found(new) self.found(new)
else: else:
@@ -153,7 +157,7 @@ class Newznab(NZBProvider, RSS):
return results return results
except SyntaxError: except SyntaxError:
log.error('Failed to parse XML response from Newznab: %s' % host) log.error('Failed to parse XML response from Newznab: %s', host)
return results return results
def getHosts(self): def getHosts(self):
@@ -172,17 +176,15 @@ class Newznab(NZBProvider, RSS):
return list return list
def belongsTo(self, url): def belongsTo(self, url, provider = None):
hosts = self.getHosts() hosts = self.getHosts()
for host in hosts: for host in hosts:
result = super(Newznab, self).belongsTo(url, host = host['host']) result = super(Newznab, self).belongsTo(url, host = host['host'], provider = provider)
if result: if result:
return result return result
return
def getUrl(self, host, type): def getUrl(self, host, type):
return cleanHost(host) + 'api?t=' + type return cleanHost(host) + 'api?t=' + type
@@ -194,3 +196,27 @@ class Newznab(NZBProvider, RSS):
def getApiExt(self, host): def getApiExt(self, host):
return '&apikey=%s' % host['api_key'] return '&apikey=%s' % host['api_key']
def download(self, url = '', nzb_id = ''):
host = urlparse(url).hostname
if self.limits_reached.get(host):
# Try again in 3 hours
if self.limits_reached[host] > time.time() - 10800:
return 'try_next'
try:
data = self.urlopen(url, show_error = False)
self.limits_reached[host] = False
return data
except HTTPError, e:
if e.code == 503:
response = e.read().lower()
if 'maximum api' in response or 'download limit' in response:
if not self.limits_reached.get(host):
log.error('Limit reached for newznab provider: %s', host)
self.limits_reached[host] = time.time()
return 'try_next'
log.error('Failed download from %s', (host, traceback.format_exc()))
raise
+39 -17
View File
@@ -1,6 +1,7 @@
from BeautifulSoup import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, getTitle from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -19,15 +20,15 @@ class NZBClub(NZBProvider, RSS):
'search': 'https://www.nzbclub.com/nzbfeed.aspx?%s', 'search': 'https://www.nzbclub.com/nzbfeed.aspx?%s',
} }
http_time_between_calls = 3 #seconds http_time_between_calls = 4 #seconds
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']): if self.isDisabled():
return results return results
q = '"%s" %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier')) q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
for ignored in Env.setting('ignored_words', 'searcher').split(','): for ignored in Env.setting('ignored_words', 'searcher').split(','):
q = '%s -%s' % (q, ignored.strip()) q = '%s -%s' % (q, ignored.strip())
@@ -40,7 +41,7 @@ class NZBClub(NZBProvider, RSS):
'ns': 1, 'ns': 1,
} }
cache_key = 'nzbclub.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) cache_key = 'nzbclub.%s.%s.%s' % (movie['library']['identifier'], quality.get('identifier'), q)
data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params)) data = self.getCache(cache_key, self.urls['search'] % tryUrlencode(params))
if data: if data:
try: try:
@@ -48,7 +49,7 @@ class NZBClub(NZBProvider, RSS):
data = XMLTree.fromstring(data) data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item') nzbs = self.getElements(data, 'channel/item')
except Exception, e: except Exception, e:
log.debug('%s, %s' % (self.getName(), e)) log.debug('%s, %s', (self.getName(), e))
return results return results
for nzb in nzbs: for nzb in nzbs:
@@ -58,10 +59,15 @@ class NZBClub(NZBProvider, RSS):
size = enclosure['length'] size = enclosure['length']
date = self.getTextElement(nzb, "pubDate") date = self.getTextElement(nzb, "pubDate")
full_description = self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), cache_timeout = 25920000) def extra_check(item):
html = BeautifulSoup(full_description) full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
description = toUnicode(nfo_pre.text) if nfo_pre else '' for ignored in ['ARCHIVE inside ARCHIVE', 'Incomplete', 'repair impossible']:
if ignored in full_description:
log.info('Wrong: Seems to be passworded or corrupted files: %s', new['name'])
return False
return True
new = { new = {
'id': nzbclub_id, 'id': nzbclub_id,
@@ -73,19 +79,17 @@ class NZBClub(NZBProvider, RSS):
'url': enclosure['url'].replace(' ', '_'), 'url': enclosure['url'].replace(' ', '_'),
'download': self.download, 'download': self.download,
'detail_url': self.getTextElement(nzb, "link"), 'detail_url': self.getTextElement(nzb, "link"),
'description': description, 'description': '',
'get_more_info': self.getMoreInfo,
'extra_check': extra_check
} }
new['score'] = fireEvent('score.calculate', new, movie, single = True)
if 'ARCHIVE inside ARCHIVE' in full_description:
log.info('Wrong: Seems to be passworded files: %s' % new['name'])
continue
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True) imdb_results = False, single_category = False, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new) results.append(new)
self.found(new) self.found(new)
@@ -94,3 +98,21 @@ class NZBClub(NZBProvider, RSS):
log.error('Failed to parse XML response from NZBClub') log.error('Failed to parse XML response from NZBClub')
return results return results
def getMoreInfo(self, item):
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
item['description'] = description
return item
def extraCheck(self, item):
full_description = self.getCache('nzbclub.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
if 'ARCHIVE inside ARCHIVE' in full_description:
log.info('Wrong: Seems to be passworded files: %s', item['name'])
return False
return True
+30 -14
View File
@@ -1,6 +1,7 @@
from BeautifulSoup import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode, \
simplifyString
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import tryInt, getTitle from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -9,6 +10,7 @@ from couchpotato.environment import Env
from dateutil.parser import parse from dateutil.parser import parse
import re import re
import time import time
import traceback
import xml.etree.ElementTree as XMLTree import xml.etree.ElementTree as XMLTree
log = CPLog(__name__) log = CPLog(__name__)
@@ -26,10 +28,10 @@ class NzbIndex(NZBProvider, RSS):
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.urls['api']): if self.isDisabled():
return results return results
q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier')) q = '"%s %s" %s' % (simplifyString(getTitle(movie['library'])), movie['library']['year'], quality.get('identifier'))
arguments = tryUrlencode({ arguments = tryUrlencode({
'q': q, 'q': q,
'age': Env.setting('retention', 'nzb'), 'age': Env.setting('retention', 'nzb'),
@@ -52,7 +54,7 @@ class NzbIndex(NZBProvider, RSS):
data = XMLTree.fromstring(data) data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item') nzbs = self.getElements(data, 'channel/item')
except Exception, e: except Exception, e:
log.debug('%s, %s' % (self.getName(), e)) log.debug('%s, %s', (self.getName(), e))
return results return results
for nzb in nzbs: for nzb in nzbs:
@@ -63,42 +65,56 @@ class NzbIndex(NZBProvider, RSS):
try: try:
description = self.getTextElement(nzb, "description") description = self.getTextElement(nzb, "description")
if '/nfo/' in description.lower():
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', description).group('nfo')
full_description = self.getCache('nzbindex.%s' % nzbindex_id, url = nfo_url, cache_timeout = 25920000)
html = BeautifulSoup(full_description)
description = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text)
except: except:
pass description = ''
def extra_check(new):
if '#c20000' in new['description'].lower():
log.info('Wrong: Seems to be passworded: %s', new['name'])
return False
return True
new = { new = {
'id': nzbindex_id, 'id': nzbindex_id,
'type': 'nzb', 'type': 'nzb',
'provider': self.getName(), 'provider': self.getName(),
'download': self.download,
'name': self.getTextElement(nzb, "title"), 'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))), 'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024, 'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'], 'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'), 'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': description, 'description': description,
'get_more_info': self.getMoreInfo,
'extra_check': extra_check,
'check_nzb': True, 'check_nzb': True,
} }
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True) imdb_results = False, single_category = False, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new) results.append(new)
self.found(new) self.found(new)
return results return results
except SyntaxError: except:
log.error('Failed to parse XML response from NZBMatrix.com') log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
return results return results
def getMoreInfo(self, item):
try:
if '/nfo/' in item['description'].lower():
nfo_url = re.search('href=\"(?P<nfo>.+)\" ', item['description']).group('nfo')
full_description = self.getCache('nzbindex.%s' % item['id'], url = nfo_url, cache_timeout = 25920000)
html = BeautifulSoup(full_description)
item['description'] = toUnicode(html.find('pre', attrs = {'id':'nfo0'}).text)
except:
pass
def isEnabled(self): def isEnabled(self):
return NZBProvider.isEnabled(self) and self.conf('enabled') return NZBProvider.isEnabled(self) and self.conf('enabled')
@@ -32,7 +32,7 @@ class NZBMatrix(NZBProvider, RSS):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']): if self.isDisabled():
return results return results
cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))]) cat_ids = ','.join(['%s' % x for x in self.getCatId(quality.get('identifier'))])
@@ -58,7 +58,7 @@ class NZBMatrix(NZBProvider, RSS):
data = XMLTree.fromstring(data) data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item') nzbs = self.getElements(data, 'channel/item')
except Exception, e: except Exception, e:
log.debug('%s, %s' % (self.getName(), e)) log.debug('%s, %s', (self.getName(), e))
return results return results
for nzb in nzbs: for nzb in nzbs:
@@ -83,13 +83,13 @@ class NZBMatrix(NZBProvider, RSS):
'description': self.getTextElement(nzb, "description"), 'description': self.getTextElement(nzb, "description"),
'check_nzb': True, 'check_nzb': True,
} }
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality, nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True) imdb_results = True, single_category = single_cat, single = True)
if is_correct_movie: if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new) results.append(new)
self.found(new) self.found(new)
@@ -99,6 +99,9 @@ class NZBMatrix(NZBProvider, RSS):
return results return results
def download(self, url = '', nzb_id = ''):
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
def getApiExt(self): def getApiExt(self):
return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('api_key')) return '&username=%s&apikey=%s' % (self.conf('username'), self.conf('api_key'))
@@ -1,23 +0,0 @@
from .main import X264
def start():
return X264()
config = [{
'name': 'x264',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'name': '#alt.binaries.hdtv.x264',
'description': 'HD movies only',
'options': [
{
'name': 'enabled',
'type': 'enabler',
'default': False,
},
],
},
],
}]
@@ -1,70 +0,0 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
import re
log = CPLog(__name__)
class X264(NZBProvider):
urls = {
'download': 'http://85.214.105.230/get_nzb.php?id=%s&section=hd',
'search': 'http://85.214.105.230/x264/requests.php?release=%s&status=FILLED&age=1300&sort=ID',
}
regex = '<tr class="req_filled"><td class="reqid">(?P<id>.*?)</td><td class="release">(?P<title>.*?)</td>.+?<td class="age">(?P<age>.*?)</td>'
http_time_between_calls = 2 # Seconds
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['search'].split('requests')[0]) or not quality.get('hd', False):
return results
q = '%s %s %s' % (getTitle(movie['library']), movie['library']['year'], quality.get('identifier'))
url = self.urls['search'] % tryUrlencode(q)
cache_key = 'x264.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, url)
if data:
match = re.compile(self.regex, re.DOTALL).finditer(data)
for nzb in match:
try:
age_match = re.match('((?P<day>\d+)d)', nzb.group('age'))
age = age_match.group('day')
except:
age = 1
new = {
'id': nzb.group('id'),
'name': nzb.group('title'),
'type': 'nzb',
'provider': self.getName(),
'age': tryInt(age),
'size': None,
'url': self.urls['download'] % (nzb.group('id')),
'detail_url': '',
'description': '',
'check_nzb': False,
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
def belongsTo(self, url, host = None):
match = re.match('http://85\.214\.105\.230/get_nzb\.php\?id=[0-9]*&section=hd', url)
if match:
return self
return
@@ -1,5 +1,57 @@
from couchpotato.core.helpers.variable import getImdb, md5
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import YarrProvider from couchpotato.core.providers.base import YarrProvider
import cookielib
import traceback
import urllib2
log = CPLog(__name__)
class TorrentProvider(YarrProvider): class TorrentProvider(YarrProvider):
type = 'torrent' type = 'torrent'
login_opener = None
def imdbMatch(self, url, imdbId):
if getImdb(url) == imdbId:
return True
if url[:4] == 'http':
try:
cache_key = md5(url)
data = self.getCache(cache_key, url)
except IOError:
log.error('Failed to open %s.', url)
return False
return getImdb(data) == imdbId
return False
def login(self):
try:
cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
urllib2.install_opener(opener)
f = opener.open(self.urls['login'], self.getLoginParams())
f.read()
f.close()
self.login_opener = opener
return True
except:
log.error('Failed to login %s: %s', (self.getName(), traceback.format_exc()))
return False
def loginDownload(self, url = '', nzb_id = ''):
try:
if not self.login_opener and not self.login():
log.error('Failed downloading from %s', self.getName())
return self.urlopen(url, opener = self.login_opener)
except:
log.error('Failed downloading from %s: %s', (self.getName(), traceback.format_exc()))
def getLoginParams(self):
return ''
@@ -1,4 +1,4 @@
from BeautifulSoup import BeautifulSoup from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.variable import tryInt, getTitle from couchpotato.core.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@@ -34,7 +34,7 @@ class KickAssTorrents(TorrentProvider):
def search(self, movie, quality): def search(self, movie, quality):
results = [] results = []
if self.isDisabled() or not self.isAvailable(self.urls['test']): if self.isDisabled():
return results return results
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier')) cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
@@ -47,14 +47,14 @@ class KickAssTorrents(TorrentProvider):
try: try:
html = BeautifulSoup(data) html = BeautifulSoup(data)
resultdiv = html.find('div', attrs = {'class':'tabs'}) resultdiv = html.find('div', attrs = {'class':'tabs'})
for result in resultdiv.findAll('div', recursive = False): for result in resultdiv.find_all('div', recursive = False):
if result.get('id').lower() not in cat_ids: if result.get('id').lower() not in cat_ids:
continue continue
try: try:
try: try:
for temp in result.findAll('tr'): for temp in result.find_all('tr'):
if temp['class'] is 'firstr' or not temp.get('id'): if temp['class'] is 'firstr' or not temp.get('id'):
continue continue
@@ -68,15 +68,17 @@ class KickAssTorrents(TorrentProvider):
} }
nr = 0 nr = 0
for td in temp.findAll('td'): for td in temp.find_all('td'):
column_name = table_order[nr] column_name = table_order[nr]
if column_name: if column_name:
if column_name is 'name': if column_name is 'name':
link = td.find('div', {'class': 'torrentname'}).findAll('a')[1] link = td.find('div', {'class': 'torrentname'}).find_all('a')[1]
new['id'] = temp.get('id')[-8:] new['id'] = temp.get('id')[-8:]
new['name'] = link.text new['name'] = link.text
new['url'] = td.findAll('a', 'idownload')[1]['href'] new['url'] = td.find_all('a', 'idownload')[1]['href']
if new['url'][:2] == '//':
new['url'] = 'http:%s' % new['url']
new['score'] = 20 if td.find('a', 'iverif') else 0 new['score'] = 20 if td.find('a', 'iverif') else 0
elif column_name is 'size': elif column_name is 'size':
new['size'] = self.parseSize(td.text) new['size'] = self.parseSize(td.text)
@@ -97,7 +99,7 @@ class KickAssTorrents(TorrentProvider):
results.append(new) results.append(new)
self.found(new) self.found(new)
except: except:
log.error('Failed parsing KickAssTorrents: %s' % traceback.format_exc()) log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
except: except:
pass pass
@@ -1,16 +1,16 @@
from .main import Moovee from .main import PublicHD
def start(): def start():
return Moovee() return PublicHD()
config = [{ config = [{
'name': 'moovee', 'name': 'publichd',
'groups': [ 'groups': [
{ {
'tab': 'searcher', 'tab': 'searcher',
'subtab': 'providers', 'subtab': 'providers',
'name': '#alt.binaries.moovee', 'name': 'PublicHD',
'description': 'SD movies only', 'description': 'Public Torrent site with only HD content.',
'options': [ 'options': [
{ {
'name': 'enabled', 'name': 'enabled',
@@ -20,4 +20,4 @@ config = [{
], ],
}, },
], ],
}] }]
@@ -0,0 +1,105 @@
from bs4 import BeautifulSoup
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode, toUnicode
from couchpotato.core.helpers.variable import getTitle, tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.torrent.base import TorrentProvider
from urlparse import parse_qs
import re
import traceback
log = CPLog(__name__)
class PublicHD(TorrentProvider):
urls = {
'test': 'http://publichd.eu',
'download': 'http://publichd.eu/%s',
'detail': 'http://publichd.eu/index.php?page=torrent-details&id=%s',
'search': 'http://publichd.eu/index.php',
}
cat_ids = [
([9], ['bd50']),
([5], ['1080p']),
([2], ['720p']),
([15, 16], ['brrip']),
]
cat_backup_id = 0
http_time_between_calls = 0
def search(self, movie, quality):
results = []
if self.isDisabled() or quality['hd'] != True:
return results
params = tryUrlencode({
'page':'torrents',
'search': getTitle(movie['library']) + ' ' + quality['identifier'],
'active': 1,
'category': self.getCatId(quality['identifier'])[0]
})
url = '%s?%s' % (self.urls['search'], params)
cache_key = 'publichd.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
data = self.getCache(cache_key, url)
if data:
try:
soup = BeautifulSoup(data)
results_table = soup.find('table', attrs = {'id': 'bgtorrlist2'})
entries = results_table.find_all('tr')
for result in entries[2:len(entries) - 1]:
info_url = result.find(href = re.compile('torrent-details'))
download = result.find(href = re.compile('\.torrent'))
if info_url and download:
url = parse_qs(info_url['href'])
new = {
'id': url['id'][0],
'name': info_url.string,
'type': 'torrent',
'check_nzb': False,
'description': '',
'provider': self.getName(),
'download': self.download,
'url': self.urls['download'] % download['href'],
'detail_url': self.urls['detail'] % url['id'][0],
'size': self.parseSize(result.find_all('td')[7].string),
'seeders': tryInt(result.find_all('td')[4].string),
'leechers': tryInt(result.find_all('td')[5].string),
'get_more_info': self.getMoreInfo
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie', nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
if is_correct_movie:
results.append(new)
self.found(new)
return results
except:
log.error('Failed getting results from %s: %s', (self.getName(), traceback.format_exc()))
return []
def getMoreInfo(self, item):
full_description = self.getCache('publichd.%s' % item['id'], item['detail_url'], cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('div', attrs = {'id':'torrmain'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
item['description'] = description
return item

Some files were not shown because too many files have changed in this diff Show More