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/
+5 -2
View File
@@ -35,7 +35,11 @@ class Loader(object):
settings.setFile(self.options.config_file)
# 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 == '':
self.data_dir = getDataDir()
@@ -89,7 +93,6 @@ class Loader(object):
if self.runAsDaemon():
try: self.daemon.stop()
except: pass
self.daemon.delpid()
except:
self.log.critical(traceback.format_exc())
+6 -2
View File
@@ -69,10 +69,14 @@ def getApiKey():
@app.errorhandler(404)
def page_not_found(error):
index_url = url_for('web.index')
url = getattr(request, 'path')[len(index_url):]
url = request.path[len(index_url):]
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:
time.sleep(0.1)
return 'Wrong API key used', 404
+37
View File
@@ -1,10 +1,39 @@
from flask.blueprints import Blueprint
from flask.helpers import url_for
from tornado.ioloop import IOLoop
from tornado.web import RequestHandler, asynchronous
from werkzeug.utils import redirect
api = Blueprint('api', __name__)
api_docs = {}
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):
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:
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 """
def 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.plugins.base import Plugin
from couchpotato.environment import Env
from flask import request
from tornado.ioloop import IOLoop
from uuid import uuid4
import os
import platform
@@ -18,7 +18,10 @@ log = CPLog(__name__)
class Core(Plugin):
ignore_restart = ['Core.crappyRestart', 'Core.crappyShutdown']
ignore_restart = [
'Core.restart', 'Core.shutdown',
'Updater.check', 'Updater.autoUpdate',
]
shutdown_started = False
def __init__(self):
@@ -37,12 +40,13 @@ class Core(Plugin):
'desc': 'Get version.'
})
addEvent('app.crappy_shutdown', self.crappyShutdown)
addEvent('app.crappy_restart', self.crappyRestart)
addEvent('app.shutdown', self.shutdown)
addEvent('app.restart', self.restart)
addEvent('app.load', self.launchBrowser, priority = 1)
addEvent('app.base_url', self.createBaseUrl)
addEvent('app.api_url', self.createApiUrl)
addEvent('app.version', self.version)
addEvent('app.load', self.checkDataDir)
addEvent('setting.save.core.password', self.md5Password)
addEvent('setting.save.core.api_key', self.checkApikey)
@@ -54,39 +58,35 @@ class Core(Plugin):
def checkApikey(self, value):
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):
return jsonified({
'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):
self.initShutdown()
if self.shutdown_started:
return False
def shutdown():
self.initShutdown()
IOLoop.instance().add_callback(shutdown)
return 'shutdown'
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'
def initShutdown(self, restart = False):
@@ -102,17 +102,20 @@ class Core(Plugin):
log.debug('Every plugin got shutdown event')
loop = True
starttime = time.time()
while loop:
log.debug('Asking who is running')
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:
break
elif starttime < time.time() - 30: # Always force break after 30s wait
break
running = list(set(still_running) - set(self.ignore_restart))
if len(running) > 0:
log.info('Waiting on plugins to finish: %s' % running)
log.info('Waiting on plugins to finish: %s', running)
else:
loop = False
@@ -121,11 +124,11 @@ class Core(Plugin):
log.debug('Save to shutdown/restart')
try:
request.environ.get('werkzeug.server.shutdown')()
IOLoop.instance().stop()
except RuntimeError:
pass
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)
+1 -1
View File
@@ -27,7 +27,7 @@ if Env.get('desktop'):
addEvent('app.after_shutdown', desktop.afterShutdown)
def onClose(self, event):
return fireEvent('app.crappy_shutdown', single = True)
return fireEvent('app.shutdown', single = True)
else:
+5 -5
View File
@@ -28,7 +28,7 @@ class Scheduler(Plugin):
for type in ['interval', 'cron']:
try:
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:
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'])
cron['job'] = job
except ValueError, e:
log.error("Failed adding cronjob: %s" % e)
log.error('Failed adding cronjob: %s', e)
# 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'])
interval['job'] = job
except ValueError, e:
log.error("Failed adding interval cronjob: %s" % e)
log.error('Failed adding interval cronjob: %s', e)
# Start it
log.debug('Starting scheduler')
@@ -75,7 +75,7 @@ class Scheduler(Plugin):
self.started = False
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.crons[identifier] = {
@@ -86,7 +86,7 @@ class Scheduler(Plugin):
}
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.intervals[identifier] = {
+93 -47
View File
@@ -1,5 +1,6 @@
from couchpotato.api import addApiView
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.logger import CPLog
from couchpotato.core.plugins.base import Plugin
@@ -19,6 +20,8 @@ log = CPLog(__name__)
class Updater(Plugin):
available_notified = False
def __init__(self):
if Env.get('desktop'):
@@ -28,7 +31,7 @@ class Updater(Plugin):
else:
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('updater.info', self.info)
@@ -48,17 +51,36 @@ class Updater(Plugin):
'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):
if self.isDisabled():
return
if self.updater.check():
if self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
fireEventAsync('app.crappy_restart')
else:
if self.conf('notification'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
if not self.available_notified and self.conf('notification') and not self.conf('automatic'):
fireEvent('updater.available', message = 'A new update is available', data = self.updater.info())
self.available_notified = True
return True
return False
def info(self):
return self.updater.info()
@@ -67,12 +89,22 @@ class Updater(Plugin):
return jsonified(self.updater.info())
def checkView(self):
self.check()
return self.updater.getInfo()
return jsonified({
'update_available': self.check(),
'info': self.updater.info()
})
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({
'success': self.updater.doUpdate()
'success': success
})
@@ -107,7 +139,7 @@ class BaseUpdater(Plugin):
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)
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:
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:
os.remove(full_path)
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:
full_path = os.path.join(root, dir_name)
@@ -127,7 +159,7 @@ class BaseUpdater(Plugin):
try:
os.rmdir(full_path)
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)
def doUpdate(self):
try:
log.debug('Stashing local changes')
self.repo.saveStash()
log.info('Updating to latest version')
info = self.info()
self.repo.pull()
# Delete leftover .pyc files
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
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
@@ -165,14 +193,14 @@ class GitUpdater(BaseUpdater):
if not self.version:
try:
output = self.repo.getHead() # Yes, please
log.debug('Git version output: %s' % output.hash)
log.debug('Git version output: %s', output.hash)
self.version = {
'hash': output.hash[:8],
'date': output.getDate(),
'type': 'git',
}
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 self.version
@@ -182,7 +210,7 @@ class GitUpdater(BaseUpdater):
if self.update_version:
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'):
self.repo.fetch()
@@ -194,7 +222,7 @@ class GitUpdater(BaseUpdater):
local = self.repo.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():
self.update_version = {
@@ -237,24 +265,24 @@ class SourceUpdater(BaseUpdater):
tar.close()
os.remove(destination)
self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0]))
self.removeDir(extracted_path)
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):
self.removeDir(extracted_path)
# Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version))
# Write update version to file
self.createFile(self.version_file, json.dumps(self.update_version))
return True
return True
except:
log.error('Failed updating: %s' % traceback.format_exc())
log.error('Failed updating: %s', traceback.format_exc())
self.update_failed = True
return False
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
self.deletePyc(only_excess = False)
self.deletePyc()
existing_files = []
for root, subfiles, filenames in os.walk(app_dir):
for filename in filenames:
@@ -267,18 +295,30 @@ class SourceUpdater(BaseUpdater):
if not Env.get('dev'):
try:
os.remove(tofile)
except:
pass
if os.path.isfile(tofile):
os.remove(tofile)
try:
os.renames(fromfile, tofile)
dirname = os.path.dirname(tofile)
if not os.path.isdir(dirname):
self.makeDir(dirname)
os.rename(fromfile, tofile)
try:
existing_files.remove(tofile)
except ValueError:
pass
except Exception, e:
log.error('Failed overwriting file: %s' % e)
except:
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):
@@ -297,11 +337,11 @@ class SourceUpdater(BaseUpdater):
output = json.loads(f.read())
f.close()
log.debug('Source version output: %s' % output)
log.debug('Source version output: %s', output)
self.version = output
self.version['type'] = 'source'
except Exception, e:
log.error('Failed using source updater. %s' % e)
log.error('Failed using source updater. %s', e)
return {}
return self.version
@@ -318,7 +358,7 @@ class SourceUpdater(BaseUpdater):
self.last_check = time.time()
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
@@ -333,12 +373,12 @@ class SourceUpdater(BaseUpdater):
'date': int(time.mktime(parse(commit['commit']['committer']['date']).timetuple())),
}
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 {}
class DesktopUpdater(Plugin):
class DesktopUpdater(BaseUpdater):
version = None
update_failed = False
@@ -350,9 +390,15 @@ class DesktopUpdater(Plugin):
def doUpdate(self):
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:
log.error('Failed updating desktop: %s' % traceback.format_exc())
self.update_failed = True
return False
@@ -379,7 +425,7 @@ class DesktopUpdater(Plugin):
self.last_check = time.time()
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
@@ -16,7 +16,17 @@ var UpdaterBase = new Class({
var self = this;
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){
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)
changelog = data.update_version.changelog + '#' + data.version.hash+'...'+data.update_version.hash
@@ -81,13 +91,19 @@ var UpdaterBase = new Class({
Api.request('updater.update', {
'onComplete': function(json){
if(json.success){
App.restart('Please wait while CouchPotato is being updated with more awesome stuff.', 'Updating');
App.checkAvailable.delay(500, App);
if(self.message)
self.message.destroy();
self.updating();
}
}
});
},
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):
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, data.get('type'))
@@ -30,7 +30,7 @@ config = [{
'label': 'Use for',
'default': 'both',
'type': 'dropdown',
'values': [('nzbs & torrents', 'both'), ('nzb', 'nzb'), ('torrent', 'torrent')],
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrent', 'torrent')],
},
{
'name': 'manual',
+8 -10
View File
@@ -10,38 +10,36 @@ class Blackhole(Downloader):
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')])):
return
directory = self.conf('directory')
if not directory or not os.path.isdir(directory):
log.error('No directory set for blackhole %s download.' % data.get('type'))
log.error('No directory set for blackhole %s download.', data.get('type'))
else:
try:
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if len(filedata) < 50:
log.error('No nzb available!')
if not filedata or len(filedata) < 50:
log.error('No nzb/torrent 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))
log.info('Downloading %s to %s.', (data.get('type'), fullPath))
with open(fullPath, 'wb') as f:
f.write(filedata)
return True
else:
log.info('File %s already exists.' % fullPath)
log.info('File %s already exists.', fullPath)
return True
except:
log.error('Failed to download to blackhole %s' % traceback.format_exc())
log.error('Failed to download to blackhole %s', traceback.format_exc())
pass
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
+8 -17
View File
@@ -14,12 +14,16 @@ class NZBGet(Downloader):
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')):
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')}
nzb_name = '%s.nzb' % self.createNzbName(data, movie)
@@ -37,25 +41,12 @@ class NZBGet(Downloader):
if e.errcode == 401:
log.error('Password is incorrect.')
else:
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())
log.error('Protocol Error: %s', e)
return False
if rpc.append(nzb_name, self.conf('category'), False, standard_b64encode(filedata.strip())):
log.info('NZB sent successfully to NZBGet')
return True
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
@@ -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',
'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',
'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.variable import cleanHost
from couchpotato.core.logger import CPLog
from inspect import ismethod, isfunction
from tempfile import mkstemp
import base64
import os
import re
import traceback
log = CPLog(__name__)
@@ -15,25 +10,12 @@ class Sabnzbd(Downloader):
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')):
return
log.info("Sending '%s' to SABnzbd." % data.get('name'))
if self.conf('ppDir') and data.get('imdb_id'):
try:
pp_script_fn = self.buildPp(data.get('imdb_id'))
except:
log.info("Failed to create post-processing script.")
pp_script_fn = False
if not pp_script_fn:
pp = False
else:
pp = True
else:
pp = False
log.info('Sending "%s" to SABnzbd.', data.get('name'))
params = {
'apikey': self.conf('api_key'),
@@ -42,29 +24,24 @@ class Sabnzbd(Downloader):
'nzbname': self.createNzbName(data, movie),
}
if data.get('download') and (ismethod(data.get('download')) or isfunction(data.get('download'))):
nzb_file = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
if not nzb_file or len(nzb_file) < 50:
log.error('No nzb available!')
if filedata:
if len(filedata) < 50:
log.error('No proper nzb available!')
return False
# 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'
else:
params['name'] = data.get('url')
if pp:
params['script'] = pp_script_fn
url = cleanHost(self.conf('host')) + "api?" + tryUrlencode(params)
try:
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:
data = self.urlopen(url, show_error = False)
data = self.urlopen(url, timeout = 60, show_error = False)
except:
log.error(traceback.format_exc())
return False
@@ -84,48 +61,3 @@ class Sabnzbd(Downloader):
else:
log.error("Unknown error: " + result[:40])
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',
'type': 'bool',
'default': False,
'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.helpers.encoding import isInt
from couchpotato.core.logger import CPLog
from libs import transmissionrpc
import httplib
import json
import re
import urllib2
log = CPLog(__name__)
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')):
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.
host = self.conf('host').split(':')
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
# Set parameters for Transmission
params = {
'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:
tc = transmissionrpc.Client(host[0], port = host[1], user = self.conf('username'), password = self.conf('password'))
filedata = data.get('download')(url = data.get('url'), nzb_id = data.get('id'))
torrent = tc.add_torrent(b64encode(filedata), **params)
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
if data.get('type') == 'torrent_magnet':
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
try:
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)
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True
except transmissionrpc.TransmissionError, e:
log.error('Failed to send link to transmission: %s' % e)
except Exception, err:
log.error('Failed to change settings for transfer: %s', err)
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)
except:
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):
@@ -43,7 +43,7 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs):
if not events.get(name): return
#log.debug('Firing event %s' % name)
#log.debug('Firing event %s', name)
try:
# Fire after event
@@ -53,6 +53,13 @@ def fireEvent(name, *args, **kwargs):
is_after_event = True
except: pass
# onComplete event
on_complete = False
try:
on_complete = kwargs['on_complete']
del kwargs['on_complete']
except: pass
# Return single handler
single = False
try:
@@ -93,7 +100,7 @@ def fireEvent(name, *args, **kwargs):
elif r[1]:
errorHandler(r[1])
else:
log.debug('Assume disabled eventhandler for: %s' % name)
log.debug('Assume disabled eventhandler for: %s', name)
else:
results = []
@@ -123,30 +130,29 @@ def fireEvent(name, *args, **kwargs):
modified_results = fireEvent('result.modify.%s' % name, results, single = True)
if modified_results:
log.debug('Return modified results for %s' % name)
log.debug('Return modified results for %s', name)
results = modified_results
if not is_after_event:
fireEvent('%s.after' % name, is_after_event = True)
if on_complete:
on_complete()
return results
except KeyError, e:
pass
except Exception:
log.error('%s: %s' % (name, traceback.format_exc()))
log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(name, *args, **kwargs):
#log.debug('Async "%s": %s, %s' % (name, args, kwargs))
def fireEventAsync(*args, **kwargs):
try:
e = events[name]
e.lock.acquire()
e.asynchronous = True
e.error_handler = errorHandler
e(*args, **kwargs)
e.lock.release()
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
my_thread.setDaemon(True)
my_thread.start()
return True
except Exception, e:
log.error('%s: %s' % (name, e))
log.error('%s: %s', (args[0], e))
def errorHandler(error):
etype, value, tb = error
+5 -1
View File
@@ -31,10 +31,14 @@ def toUnicode(original, *args):
except:
raise
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')
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):
if isinstance(original, (str, unicode)):
try:
+11 -7
View File
@@ -1,7 +1,7 @@
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.variable import natcmp
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 urllib import unquote
import flask
@@ -26,7 +26,7 @@ def getParams():
for item in nested:
if item is nested[-1]:
current[item] = toUnicode(unquote(value)).encode('utf-8')
current[item] = toUnicode(unquote(value))
else:
try:
current[item]
@@ -35,7 +35,7 @@ def getParams():
current = current[item]
else:
temp[param] = toUnicode(unquote(value)).encode('utf-8')
temp[param] = toUnicode(unquote(value))
return dictToList(temp)
@@ -57,7 +57,7 @@ def dictToList(params):
def getParam(attr, default = None):
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:
return default
@@ -70,9 +70,13 @@ def jsonify(mimetype, *args, **kwargs):
return getattr(current_app, 'response_class')(content, mimetype = mimetype)
def jsonified(*args, **kwargs):
from couchpotato.environment import Env
callback = getParam('callback_func', None)
if callback:
return padded_jsonify(callback, *args, **kwargs)
content = padded_jsonify(callback, *args, **kwargs)
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:
return XMLTree.parse(data).findall(path)
except Exception, e:
log.error('Error parsing RSS. %s' % e)
log.error('Error parsing RSS. %s', e)
return []
+15 -5
View File
@@ -2,7 +2,10 @@ from couchpotato.core.logger import CPLog
import hashlib
import os.path
import platform
import random
import re
import string
import sys
log = CPLog(__name__)
@@ -20,6 +23,10 @@ def getDataDir():
if 'darwin' in platform.platform().lower():
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
return os.path.join(user_dir, '.couchpotato')
@@ -77,9 +84,9 @@ def cleanHost(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')
txt = output.read()
output.close()
@@ -94,7 +101,7 @@ def getImdb(txt):
def tryInt(s):
try: return int(s)
except: return s
except: return 0
def tryFloat(s):
try: return float(s) if '.' in s else tryInt(s)
@@ -111,9 +118,12 @@ def getTitle(library_dict):
try:
return library_dict['titles'][0]['title']
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
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
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:
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
did_save += self.loadSettings(m, module_name, save = False)
@@ -57,10 +57,10 @@ class Loader(object):
log.error(e.message)
pass
# todo:: this needs to be more descriptive.
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.error('Import error, remove the empty folder: %s', plugin.get('module'))
log.debug('Can\'t import %s: %s', (module_name, traceback.format_exc()))
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:
fireEvent('settings.save')
@@ -84,7 +84,7 @@ class Loader(object):
fireEvent('settings.register', section_name = section['name'], options = options, save = save)
return True
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
def loadPlugins(self, module, name):
@@ -97,7 +97,7 @@ class Loader(object):
return True
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
def addModule(self, priority, plugin_type, module, name):
+29 -17
View File
@@ -1,5 +1,6 @@
import logging
import re
import traceback
class CPLog(object):
@@ -13,31 +14,42 @@ class CPLog(object):
self.context = context
self.logger = logging.getLogger()
def info(self, msg):
self.logger.info(self.addContext(msg))
def info(self, msg, replace_tuple = ()):
self.logger.info(self.addContext(msg, replace_tuple))
def debug(self, msg):
self.logger.debug(self.addContext(msg))
def debug(self, msg, replace_tuple = ()):
self.logger.debug(self.addContext(msg, replace_tuple))
def error(self, msg):
self.logger.error(self.addContext(msg))
def error(self, msg, replace_tuple = ()):
self.logger.error(self.addContext(msg, replace_tuple))
def warning(self, msg):
self.logger.warning(self.addContext(msg))
def warning(self, msg, replace_tuple = ()):
self.logger.warning(self.addContext(msg, replace_tuple))
def critical(self, msg):
self.logger.critical(self.addContext(msg), exc_info = 1)
def critical(self, msg, replace_tuple = ()):
self.logger.critical(self.addContext(msg, replace_tuple), exc_info = 1)
def addContext(self, msg):
return '[%+25.25s] %s' % (self.context[-25:], self.removePrivateData(msg))
def addContext(self, msg, replace_tuple = ()):
return '[%+25.25s] %s' % (self.context[-25:], self.safeMessage(msg, replace_tuple))
def removePrivateData(self, msg):
try:
msg = unicode(msg)
except:
pass
def safeMessage(self, msg, replace_tuple = ()):
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'):
for replace in self.replace_private:
+5 -2
View File
@@ -13,7 +13,10 @@ class Notification(Plugin):
default_title = Env.get('appname')
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 = []
def __init__(self):
@@ -41,7 +44,7 @@ class Notification(Plugin):
test_type = self.testNotifyName()
log.info('Sending test to %s' % test_type)
log.info('Sending test to %s', test_type)
success = self.notify(
message = self.test_message,
+95 -21
View File
@@ -1,6 +1,6 @@
from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam
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.settings.model import Notification as Notif
from sqlalchemy.sql.expression import or_
import threading
import time
import uuid
log = CPLog(__name__)
class CoreNotifier(Notification):
m_lock = threading.Lock()
messages = []
listeners = []
listen_to = [
'movie.downloaded', 'movie.snatched',
'updater.available', 'updater.updated',
@@ -30,7 +35,7 @@ class CoreNotifier(Notification):
addApiView('notification.markread', self.markAsRead, docs = {
'desc': 'Mark notifications as read',
'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)
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):
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()
q = db.query(Notif) \
.filter(or_(*[Notif.id == tryInt(s) for s in ids]))
if 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})
db.commit()
@@ -83,6 +97,8 @@ class CoreNotifier(Notification):
limit = splt[0]
offset = 0 if len(splt) is 1 else splt[1]
q = q.limit(limit).offset(offset)
else:
q = q.limit(200)
results = q.all()
notifications = []
@@ -114,25 +130,86 @@ class CoreNotifier(Notification):
ndict = n.to_dict()
ndict['type'] = 'notification'
ndict['time'] = time.time()
self.messages.append(ndict)
self.frontend(type = listener, data = data)
#db.close()
return True
def frontend(self, type = 'notification', data = {}):
self.messages.append({
def frontend(self, type = 'notification', data = {}, message = None):
self.m_lock.acquire()
notification = {
'message_id': str(uuid.uuid4()),
'time': time.time(),
'type': type,
'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):
messages = []
for message in self.messages:
#delete message older then 15s
if message['time'] > (time.time() - 15):
messages.append(message)
# Get unread
if getParam('init'):
@@ -146,9 +223,6 @@ class CoreNotifier(Notification):
ndict['type'] = 'notification'
messages.append(ndict)
#db.close()
self.messages = []
return jsonified({
'success': True,
'result': messages,
@@ -8,9 +8,10 @@ var NotificationBase = new Class({
self.setOptions(options);
// Listener
App.addEvent('load', self.startInterval.bind(self));
App.addEvent('unload', self.stopTimer.bind(self));
App.addEvent('unload', self.stopPoll.bind(self));
App.addEvent('reload', self.startInterval.bind(self, [true]));
App.addEvent('notification', self.notify.bind(self));
App.addEvent('message', self.showMessage.bind(self));
// Add test buttons to settings page
App.addEvent('load', self.addTestButtons.bind(self));
@@ -30,7 +31,11 @@ var NotificationBase = new Class({
'href': App.createUrl('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)
Api.request('notification.markread', {
'data': {
'ids': ids.join(',')
},
'onSuccess': function(){
self.setBadge('')
}
@@ -83,39 +85,88 @@ var NotificationBase = new Class({
},
startInterval: function(){
startInterval: function(force){
var self = this;
self.request = Api.request('notification.listener', {
'initialDelay': 100,
'delay': 3000,
if(self.stopped && !force){
self.stopped = false;
return;
}
Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': self.processData.bind(self)
})
self.request.startTimer()
}).send()
},
startTimer: function(){
if(this.request)
this.request.startTimer()
startPoll: function(){
var self = this;
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)
this.request.stopTimer()
this.request.cancel()
this.stopped = true;
},
processData: function(json){
var self = this;
self.request.options.data = {}
Array.each(json.result, function(result){
App.fireEvent(result.type, result)
})
// Process data
if(json){
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(){
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.notifications.base import Notification
from couchpotato.environment import Env
from gntp import notifier
import logging
import traceback
log = CPLog(__name__)
@@ -17,7 +16,7 @@ class Growl(Notification):
super(Growl, self).__init__()
if self.isEnabled():
self.register()
addEvent('app.load', self.register)
def register(self):
if self.registered: return
@@ -39,7 +38,7 @@ class Growl(Notification):
self.growl.register()
self.registered = True
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):
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:
terminal = telnetlib.Telnet(host)
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()
log.debug('Connected to %s via telnet' % (host))
log.debug('Connected to %s via telnet', (host))
terminal.read_until('sh-3.00# ')
terminal.write('cat /tmp/source\n')
terminal.write('cat /tmp/netshare\n')
@@ -48,9 +48,9 @@ class NMJ(Notification):
if match:
database = match.group(1)
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:
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()
if device.startswith('NETWORK_SHARE/'):
@@ -58,7 +58,7 @@ class NMJ(Notification):
if match:
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:
log.error('Detected a network share on the Popcorn Hour, but could not get the mounting url')
return self.failed()
@@ -77,7 +77,7 @@ class NMJ(Notification):
database = self.conf('database')
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:
data = self.urlopen(mount)
except:
@@ -102,11 +102,11 @@ class NMJ(Notification):
et = etree.fromstring(response)
result = et.findtext('returnValue')
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
if int(result) > 0:
log.error('Popcorn Hour returned an errorcode: %s' % (result))
log.error('Popcorn Hour returned an errorcode: %s', (result))
return False
else:
log.info('NMJ started background scan')
@@ -32,7 +32,7 @@ class Notifo(Notification):
raise Exception
except:
log.error('Notification failed: %s' % traceback.format_exc())
log.error('Notification failed: %s', traceback.format_exc())
return False
log.info('Notifo notification successful.')
@@ -23,6 +23,6 @@ class NotifyMyAndroid(Notification):
for key in keys:
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
@@ -17,7 +17,7 @@ class NotifyMyWP(Notification):
for key in keys:
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 response
@@ -17,8 +17,9 @@ config = [{
},
{
'name': 'host',
'default': 'localhost:32400',
'description': 'Default should be on localhost:32400',
'default': 'localhost',
'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.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
@@ -11,13 +12,14 @@ log = CPLog(__name__)
class Plex(Notification):
def __init__(self):
super(Plex, self).__init__()
addEvent('renamer.after', self.addToLibrary)
def addToLibrary(self, group = {}):
if self.isDisabled(): return
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:
@@ -36,7 +38,33 @@ class Plex(Notification):
x = self.urlopen(url)
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 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.')
return True
elif request_status == 401:
log.error('Prowl auth failed: %s' % response.reason)
log.error('Prowl auth failed: %s', response.reason)
return False
else:
log.error('Prowl notification failed.')
@@ -35,7 +35,7 @@ class Pushover(Notification):
log.info('Pushover notifications sent.')
return True
elif request_status == 401:
log.error('Pushover auth failed: %s' % response.reason)
log.error('Pushover auth failed: %s', response.reason)
return False
else:
log.error('Pushover notification failed.')
@@ -15,14 +15,14 @@ class Synoindex(Notification):
if self.isDisabled(): return
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:
p = subprocess.Popen(command, stdout = subprocess.PIPE, stderr = subprocess.STDOUT)
out = p.communicate()
log.info('Result from synoindex: %s' % str(out))
log.info('Result from synoindex: %s', str(out))
return True
except OSError, e:
log.error('Unable to run synoindex: %s' % e)
log.error('Unable to run synoindex: %s', e)
return False
return True
@@ -55,7 +55,7 @@ class Twitter(Notification):
else:
api.PostUpdate('[%s] %s' % (self.default_title, message))
except Exception, e:
log.error('Error sending tweet: %s' % e)
log.error('Error sending tweet: %s', e)
return False
return True
@@ -71,7 +71,7 @@ class Twitter(Notification):
resp, content = oauth_client.request(self.urls['request'], 'POST', body = tryUrlencode({'oauth_callback': callback_url}))
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({
'success': False,
})
@@ -80,7 +80,7 @@ class Twitter(Notification):
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({
'success': True,
'url': auth_url,
@@ -100,10 +100,10 @@ class Twitter(Notification):
access_token = dict(parse_qsl(content))
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'
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_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")
return False
log.info('XBMC notification to %s successful.' % host)
log.info('XBMC notification to %s successful.', host)
return True
@@ -25,7 +25,7 @@ config = [{
},
{
'name': 'rating',
'default': 6.0,
'default': 7.0,
'type': 'float',
},
{
+5 -1
View File
@@ -19,4 +19,8 @@ class Automation(Plugin):
movies = fireEvent('automation.get_movies', merge = True)
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.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.logger import CPLog
from couchpotato.environment import Env
@@ -71,6 +71,7 @@ class Plugin(object):
return send_from_directory(d, filename)
def createFile(self, path, content, binary = False):
path = ss(path)
self.makeDir(os.path.dirname(path))
@@ -80,20 +81,21 @@ class Plugin(object):
f.close()
os.chmod(path, Env.getPermission('file'))
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):
path = ss(path)
try:
if not os.path.isdir(path):
os.makedirs(path, Env.getPermission('folder'))
return True
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
# 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
if not headers.get('Referer'):
@@ -106,7 +108,7 @@ class Plugin(object):
# Don't try for failed requests
if self.http_failed_disabled.get(host, 0) > 0:
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
else:
del self.http_failed_request[host]
@@ -116,7 +118,7 @@ class Plugin(object):
try:
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)
cookies = cookielib.CookieJar()
@@ -124,16 +126,19 @@ class Plugin(object):
data = opener.open(request, timeout = timeout).read()
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
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
except IOError:
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
try:
@@ -147,7 +152,7 @@ class Plugin(object):
self.http_failed_disabled[host] = time.time()
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
@@ -163,7 +168,7 @@ class Plugin(object):
wait = math.ceil(last_use - now + self.http_time_between_calls)
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)
def beforeCall(self, handler):
@@ -199,9 +204,10 @@ class Plugin(object):
def getCache(self, cache_key, url = None, **kwargs):
cache_key = simplifyString(cache_key)
cache = Env.get('cache').get(cache_key)
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
if url:
@@ -213,13 +219,15 @@ class Plugin(object):
del kwargs['cache_timeout']
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
except:
pass
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)
return value
+4 -2
View File
@@ -62,13 +62,15 @@ class FileBrowser(Plugin):
def view(self):
path = getParam('path', '/')
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:
dirs = []
return jsonified({
'is_root': getParam('path', '/') == '/',
'is_root': path == '/' or not path,
'empty': len(dirs) == 0,
'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.environment import Env
import os.path
import traceback
log = CPLog(__name__)
@@ -46,6 +47,7 @@ class FileManager(Plugin):
try:
filedata = self.urlopen(url, **urlopen_kwargs)
except:
log.error('Failed downloading file %s: %s', (url, traceback.format_exc()))
return False
self.createFile(dest, filedata, binary = True)
+16 -13
View File
@@ -78,7 +78,7 @@ class LibraryPlugin(Plugin):
except: pass
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
# Main info
@@ -95,7 +95,7 @@ class LibraryPlugin(Plugin):
db.commit()
titles = info.get('titles', [])
log.debug('Adding titles: %s' % titles)
log.debug('Adding titles: %s', titles)
for title in titles:
if not title:
continue
@@ -117,26 +117,29 @@ class LibraryPlugin(Plugin):
continue
file_path = fireEvent('file.download', url = image, single = True)
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
except:
log.debug('Failed to attach to library: %s' % traceback.format_exc())
if file_path:
file_obj = fireEvent('file.add', path = file_path, type_tuple = ('image', type), single = True)
try:
file_obj = db.query(File).filter_by(id = file_obj.get('id')).one()
library.files.append(file_obj)
db.commit()
except:
log.debug('Failed to attach to library: %s', traceback.format_exc())
library_dict = library.to_dict(self.default_dict)
fireEvent('library.update_finish', data = library_dict)
#db.close()
return library_dict
def updateReleaseDate(self, identifier):
db = get_session()
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():
dates = fireEvent('movie.release_date', identifier = identifier, merge = True)
+6 -5
View File
@@ -1,5 +1,7 @@
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.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
@@ -71,14 +73,14 @@ class Logging(Plugin):
return jsonified({
'success': True,
'log': log,
'log': toUnicode(log),
'total': total,
})
def partial(self):
log_type = getParam('type', 'all')
total_lines = getParam('lines', 30)
total_lines = tryInt(getParam('lines', 30))
log_lines = []
@@ -92,13 +94,12 @@ class Logging(Plugin):
reversed_lines = []
f = open(path, 'r')
reversed_lines = f.read().split('[0m\n')
reversed_lines = toUnicode(f.read()).split('[0m\n')
reversed_lines.reverse()
brk = False
for line in reversed_lines:
#print '%s ' % log_type in line.lower()
if log_type == 'all' or '%s ' % log_type.upper() in line:
log_lines.append(line)
@@ -149,7 +150,7 @@ class Logging(Plugin):
except:
log.error(log_message)
except:
log.error('Couldn\'t log via API: %s' % params)
log.error('Couldn\'t log via API: %s', params)
return jsonified({
@@ -59,6 +59,7 @@
width: 14%;
color: lightgrey;
padding: 3px 0;
font-size: 10px;
}
.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();
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', {
'text': i+1,
'class': nr == i ? 'active': '',
@@ -78,11 +78,16 @@ Page.Log = new Class({
addColors: function(text){
var self = this;
text = text.replace(/\u001b\[31m/gi, '</span><span class="error">')
text = text.replace(/\u001b\[36m/gi, '</span><span class="debug">')
text = text.replace(/\u001b\[33m/gi, '</span><span class="debug">')
text = text.replace(/\u001b\[0m\n/gi, '</span><span class="time">')
text = text.replace(/\u001b\[0m/gi, '</span><span>')
text = text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.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>';
}
+11 -10
View File
@@ -1,6 +1,6 @@
from couchpotato.api import addApiView
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.plugins.base import Plugin
from couchpotato.environment import Env
@@ -25,13 +25,14 @@ class Manage(Plugin):
})
if not Env.get('dev'):
addEvent('app.load', self.updateLibrary)
def updateLibrary():
self.updateLibrary(full = False)
addEvent('app.load', updateLibrary)
def updateLibraryView(self):
params = getParams()
fireEventAsync('manage.update', full = params.get('full', True))
full = getParam('full', default = 1)
fireEventAsync('manage.update', full = True if full == '1' else False)
return jsonified({
'success': True
@@ -51,11 +52,11 @@ class Manage(Plugin):
if not os.path.isdir(directory):
if len(directory) > 0:
log.error('Directory doesn\'t exist: %s' % directory)
log.error('Directory doesn\'t exist: %s', directory)
continue
log.info('Updating manage library: %s' % directory)
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update, single = True)
log.info('Updating manage library: %s', directory)
identifiers = fireEvent('scanner.folder', folder = directory, newer_than = last_update if not full else 0, single = True)
if identifiers:
added_identifiers.extend(identifiers)
@@ -67,11 +68,11 @@ class Manage(Plugin):
if self.conf('cleanup') and full and not self.shuttingDown():
# 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:
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())
+63 -20
View File
@@ -130,6 +130,8 @@ class MoviePlugin(Plugin):
.filter(or_(*[Movie.status.has(identifier = s) for s in status])) \
.group_by(Movie.id)
total_count = q.count()
filter_or = []
if starts_with:
starts_with = toUnicode(starts_with.lower())
@@ -156,8 +158,7 @@ class MoviePlugin(Plugin):
.options(joinedload_all('library.titles')) \
.options(joinedload_all('library.files')) \
.options(joinedload_all('status')) \
.options(joinedload_all('files')) \
.options(joinedload_all('files'))
if limit_offset:
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]
q2 = q2.limit(limit).offset(offset)
results = q2.all()
movies = []
for movie in results:
@@ -178,7 +178,7 @@ class MoviePlugin(Plugin):
movies.append(temp)
#db.close()
return movies
return (total_count, movies)
def availableChars(self, status = ['active']):
@@ -214,11 +214,12 @@ class MoviePlugin(Plugin):
starts_with = params.get('starts_with', 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({
'success': True,
'empty': len(movies) == 0,
'total': total_movies,
'movies': movies,
})
@@ -241,14 +242,16 @@ class MoviePlugin(Plugin):
for id in getParam('id').split(','):
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:
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()
return jsonified({
@@ -277,6 +280,10 @@ class MoviePlugin(Plugin):
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)
# Status
@@ -287,6 +294,7 @@ class MoviePlugin(Plugin):
db = get_session()
m = db.query(Movie).filter_by(library_id = library.get('id')).first()
added = True
do_search = False
if not m:
m = Movie(
@@ -295,8 +303,14 @@ class MoviePlugin(Plugin):
status_id = status_active.get('id'),
)
db.add(m)
fireEvent('library.update', params.get('identifier'), default_title = params.get('title', ''))
do_search = True
db.commit()
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:
# Clean snatched history
for release in m.releases:
@@ -305,10 +319,12 @@ class MoviePlugin(Plugin):
m.profile_id = params.get('profile_id', default_profile.get('id'))
else:
log.debug('Movie already exists, not updating: %s' % params)
log.debug('Movie already exists, not updating: %s', params)
added = False
if force_readd:
m.status_id = status_active.get('id')
do_search = True
db.commit()
@@ -321,8 +337,12 @@ class MoviePlugin(Plugin):
movie_dict = m.to_dict(self.default_dict)
if (force_readd or do_search) and search_after:
fireEventAsync('searcher.single', movie_dict)
if do_search and search_after:
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()
return movie_dict
@@ -336,7 +356,7 @@ class MoviePlugin(Plugin):
return jsonified({
'success': True,
'added': True,
'added': True if movie_dict else False,
'movie': movie_dict,
})
@@ -351,6 +371,9 @@ class MoviePlugin(Plugin):
for movie_id in ids:
m = db.query(Movie).filter_by(id = movie_id).first()
if not m:
continue
m.profile_id = params.get('profile_id')
# Remove releases
@@ -369,7 +392,7 @@ class MoviePlugin(Plugin):
fireEvent('movie.restatus', m.id)
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()
return jsonified({
@@ -421,6 +444,7 @@ class MoviePlugin(Plugin):
db.commit()
elif new_movie_status:
new_status = fireEvent('status.get', new_movie_status, single = True)
movie.profile_id = None
movie.status_id = new_status.get('id')
db.commit()
else:
@@ -441,7 +465,7 @@ class MoviePlugin(Plugin):
log.debug('Can\'t restatus movie, doesn\'t seem to exist.')
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:
m.status_id = done_status.get('id')
else:
@@ -458,3 +482,22 @@ class MoviePlugin(Plugin):
#db.close()
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: {
navigation: true,
limit: 50,
menu: []
menu: [],
add_new: false
},
movies: [],
movies_added: {},
letters: {},
filter: {
'startswith': null,
@@ -30,6 +32,17 @@ var MovieList = new Class({
})
);
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(){
@@ -59,7 +72,7 @@ var MovieList = new Class({
self.created = true;
},
addMovies: function(movies){
addMovies: function(movies, total){
var self = this;
if(!self.created) self.create();
@@ -71,25 +84,41 @@ var MovieList = new Class({
}
Object.each(movies, function(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.createMovie(movie);
});
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(){
var self = this;
@@ -100,6 +129,7 @@ var MovieList = new Class({
self.navigation = new Element('div.alph_nav').adopt(
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', {
'events': {
'click:relay(li)': function(e, el){
@@ -260,6 +290,7 @@ var MovieList = new Class({
'events': {
'click': function(e){
(e).preventDefault();
this.set('text', 'Deleting..')
Api.request('movie.delete', {
'data': {
'id': ids.join(','),
@@ -268,14 +299,19 @@ var MovieList = new Class({
'onSuccess': function(){
qObj.close();
var erase_movies = [];
self.movies.each(function(movie){
if (movie.isSelected()){
$(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),
'onComplete': function(json){
self.store(json.movies);
self.addMovies(json.movies);
self.addMovies(json.movies, json.total);
self.load_more.set('text', 'load more movies');
if(self.scrollspy) self.scrollspy.start();
}
@@ -24,6 +24,10 @@
.movies .movie.list_view:hover, .movies .movie.mass_edit_view:hover {
background: rgba(255,255,255,0.03);
}
.movies .movie_container {
overflow: hidden;
}
.movies .data {
padding: 20px;
@@ -32,7 +36,6 @@
position: relative;
float: right;
border-radius: 0;
overflow: hidden;
transition: all 0.2s linear;
}
.movies .list_view .data, .movies .mass_edit_view .data {
@@ -89,7 +92,7 @@
font-size: 16px;
font-weight: normal;
text-overflow: ellipsis;
width: 64%;
width: auto;
}
.movies .info .year {
@@ -152,7 +155,6 @@
.movies .list_view .data .quality, .movies .mass_edit_view .data .quality {
text-align: right;
float: right;
width: 30%;
}
.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 {
margin: -34px 2px 0 0;
background: #4e5969;
position: relative;
}
.movies .delete_container {
@@ -310,6 +313,33 @@
padding-bottom: 4px;
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 {
display: block;
@@ -330,15 +360,17 @@
width: 1080px;
margin: 0 -60px;
box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1);
background: #4e5969;
}
.movies .alph_nav.float {
box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5);
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;
padding: 0 0 1px;
margin: 0;
@@ -346,10 +378,15 @@
user-select: none;
}
.movies .alph_nav .counter {
width: 60px;
text-align: center;
}
.movies .alph_nav .numbers li, .movies .alph_nav .actions li {
display: inline-block;
vertical-align: top;
width: 22px;
width: 20px;
height: 24px;
line-height: 26px;
text-align: center;
@@ -361,7 +398,6 @@
}
.movies .alph_nav .numbers li:first-child {
width: 43px;
margin-left: 7px;
}
.movies .alph_nav li.available {
color: rgba(255,255,255,0.8);
@@ -370,8 +406,8 @@
}
.movies .alph_nav li.active.available, .movies .alph_nav li.available:hover {
color: #fff;
font-size: 24px;
line-height: 24px;
font-size: 20px;
line-height: 20px;
}
.movies .alph_nav input {
+221 -41
View File
@@ -11,53 +11,124 @@ var Movie = new Class({
self.view = options.view || 'thumbs';
self.list = list;
self.el = new Element('div.movie.inlay');
self.profile = Quality.getProfile(data.profile_id) || {};
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(){
var self = this;
self.el = new Element('div.movie.inlay').adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'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.el.adopt(
self.container = new Element('div.movie_container').adopt(
self.select_checkbox = new Element('input[type=checkbox].inlay', {
'events': {
'change': function(){
self.fireEvent('select')
}
})
),
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.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);
@@ -178,6 +249,10 @@ var Movie = new Class({
isSelected: function(){
return this.select_checkbox.get('checked');
},
toElement: function(){
return this.el;
}
});
@@ -290,7 +365,7 @@ var ReleaseAction = new Class({
} catch(e){}
new Element('div', {
'class': 'item'
'class': 'item '+status.identifier
}).adopt(
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}),
@@ -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(
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,
new Element('div.info').adopt(
self.title = new Element('h2', {
@@ -299,11 +301,11 @@ Block.Search.Item = new Class({
'title': self.title_select.get('value'),
'profile_id': self.profile_select.get('value')
},
'onComplete': function(){
'onComplete': function(json){
self.options.empty();
self.options.adopt(
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(
new Element('div').adopt(
self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0]
self.option_thumbnail = self.info.images && self.info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': self.info.images.poster[0],
'height': null,
'width': null
}) : null,
self.info.in_wanted ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
+2 -3
View File
@@ -135,8 +135,7 @@ class ProfilePlugin(Plugin):
success = True
except Exception, e:
message = 'Failed deleting Profile: %s' % e
log.error(message)
message = log.error('Failed deleting Profile: %s', e)
#db.close()
@@ -163,7 +162,7 @@ class ProfilePlugin(Plugin):
# Create default quality profile
order = -2
for profile in profiles:
log.info('Creating default profile: %s' % profile.get('label'))
log.info('Creating default profile: %s', profile.get('label'))
p = Profile(
label = toUnicode(profile.get('label')),
order = order
+11 -11
View File
@@ -17,8 +17,8 @@ class QualityPlugin(Plugin):
qualities = [
{'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': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts', 'ts']},
{'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', 'ts']},
{'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': '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': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': [], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
{'identifier': 'cam', 'size': (600, 1000), 'label': 'Cam', 'alternative': ['camrip', 'hdcam'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}
]
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()
if not quality:
log.info('Creating quality: %s' % q.get('label'))
log.info('Creating quality: %s', q.get('label'))
quality = Quality()
db.add(quality)
@@ -133,7 +133,7 @@ class QualityPlugin(Plugin):
).all()
if not profile:
log.info('Creating profile: %s' % q.get('label'))
log.info('Creating profile: %s', q.get('label'))
profile = Profile(
core = True,
label = toUnicode(quality.label),
@@ -170,20 +170,20 @@ class QualityPlugin(Plugin):
# Check tags
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)
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)
for tag in quality.get('tags', []):
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)
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)
# Try again with loose testing
@@ -191,7 +191,7 @@ class QualityPlugin(Plugin):
if 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
def guessLoose(self, hash, extra):
@@ -200,7 +200,7 @@ class QualityPlugin(Plugin):
# Last check on resolution only
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)
if 480 <= extra.get('resolution_width', 0) <= 720:
+3 -3
View File
@@ -79,7 +79,7 @@ class Release(Plugin):
rel.files.append(added_file)
db.commit()
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)
@@ -143,7 +143,7 @@ class Release(Plugin):
item[info.identifier] = info.value
# 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
fireEvent('searcher.download', data = item, movie = rel.movie.to_dict({
@@ -158,7 +158,7 @@ class Release(Plugin):
'success': True
})
else:
log.error('Couldn\'t find release with id: %s' % id)
log.error('Couldn\'t find release with id: %s', id)
#db.close()
return jsonified({
@@ -19,6 +19,9 @@ rename_options = {
'source': 'Source media (Bluray)',
'original': 'Original filename',
'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
if not group['library'] or not movie_title:
if group['dirname']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_UNKNOWN_%s' % 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, '_UNKNOWN_%s' % filename)
self.tagDir(group, 'unknown')
continue
# Rename the files using the library data
else:
group['library'] = fireEvent('library.update', identifier = group['library']['identifier'], single = True)
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
library = group['library']
@@ -129,13 +123,14 @@ class Renamer(Plugin):
'source': group['meta_data']['source'],
'resolution_width': group['meta_data'].get('resolution_width'),
'resolution_height': group['meta_data'].get('resolution_height'),
'imdb_id': library['identifier'],
}
for file_type in group['files']:
# Move nfo depending on settings
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'):
for current_file in group['files'][file_type]:
remove_files.append(current_file)
@@ -194,7 +189,7 @@ class Renamer(Plugin):
break
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
else:
@@ -208,7 +203,7 @@ class Renamer(Plugin):
if file_type is 'subtitle':
# 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, [])
rename_extras = self.getRenameExtras(
@@ -263,13 +258,13 @@ class Renamer(Plugin):
# Mark movie "done" onces it found the quality with the finish check
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:
if profile_type.quality_id == group['meta_data']['quality']['id'] and profile_type.finish:
movie.status_id = done_status.get('id')
db.commit()
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
for release in movie.releases:
@@ -279,30 +274,23 @@ class Renamer(Plugin):
# This is where CP removes older, lesser quality releases
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:
remove_files.append(current_file)
remove_releases.append(release)
# Same quality, but still downloaded, so maybe repack/proper/unrated/directors cut etc
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:
remove_files.append(current_file)
remove_releases.append(release)
# Downloaded a lower quality, rename the newly downloaded files/folder to exclude them from scan
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
if group['dirname']:
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)
self.tagDir(group, 'exists')
# 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)
@@ -311,7 +299,6 @@ class Renamer(Plugin):
break
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']:
log.debug('Marking release as downloaded')
release.status_id = downloaded_status.get('id')
@@ -323,14 +310,32 @@ class Renamer(Plugin):
for current_file in group['files']['leftover']:
remove_files.append(current_file)
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
group['renamed_files'] = []
for src in rename_files:
if 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
self.makeDir(os.path.dirname(dst))
@@ -339,34 +344,23 @@ class Renamer(Plugin):
self.moveFile(src, dst)
group['renamed_files'].append(dst)
except:
log.error('Failed moving the file "%s" : %s' % (os.path.basename(src), traceback.format_exc()))
# 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()))
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
self.tagDir(group, 'failed_rename')
# Remove matching releases
for release in remove_releases:
log.debug('Removing release %s' % release.identifier)
log.debug('Removing release %s', release.identifier)
try:
db.delete(release)
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']:
try:
log.info('Deleting folder: %s' % group['parentdir'])
log.info('Deleting folder: %s', group['parentdir'])
self.deleteEmptyFolder(group['parentdir'])
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
fireEventAsync('renamer.after', group)
@@ -398,17 +392,43 @@ class Renamer(Plugin):
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):
try:
shutil.move(old, dest)
try:
os.chmod(dest, Env.getPermission('folder'))
os.chmod(dest, Env.getPermission('file'))
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:
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
return True
@@ -421,7 +441,7 @@ class Renamer(Plugin):
replaced = toUnicode(string)
for x, r in replacements.iteritems():
if r is not None:
replaced = replaced.replace('<%s>' % toUnicode(x), toUnicode(r))
replaced = replaced.replace(u'<%s>' % toUnicode(x), toUnicode(r))
else:
#If information is not available, we don't want the tag in the filename
replaced = replaced.replace('<' + x + '>', '')
@@ -444,9 +464,9 @@ class Renamer(Plugin):
try:
os.rmdir(full_path)
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:
os.rmdir(folder)
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.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.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import File
from couchpotato.environment import Env
from couchpotato.core.settings.model import File, Movie
from enzyme.exceptions import NoParserError, ParseError
from guessit import guess_movie_info
from subliminal.videos import Video
@@ -24,10 +23,10 @@ class Scanner(Plugin):
'media': 314572800, # 300MB
'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']
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'],
'dvd': ['vts_*', 'vob'],
'nfo': ['nfo', 'txt', 'tag'],
@@ -89,24 +88,28 @@ class Scanner(Plugin):
addEvent('scanner.partnumber', self.getPartNumber)
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):
folder = os.path.normpath(folder)
groups = self.scan(folder = folder, files = files)
for group in groups.itervalues():
if group['library']:
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):
return
groups = self.scan(folder = folder, simple = simple)
groups = self.scan(folder = folder, simple = simple, newer_than = newer_than)
added_identifier = []
while True and not self.shuttingDown():
@@ -127,10 +130,12 @@ class Scanner(Plugin):
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):
log.error('Folder doesn\'t exists: %s' % folder)
log.error('Folder doesn\'t exists: %s', folder)
return {}
# Get movie "master" files
@@ -141,19 +146,15 @@ class Scanner(Plugin):
if len(files) == 0:
try:
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:
files.append(os.path.join(root, filename))
except:
try:
files = []
folder = toUnicode(folder).encode(Env.get('encoding'))
log.info('Trying to convert unicode to str path: %s, %s' % (folder, type(folder)))
for root, dirs, walk_files in os.walk(folder):
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()))
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
files = [ss(x) for x in files]
db = get_session()
for file_path in files:
@@ -207,7 +208,7 @@ class Scanner(Plugin):
for identifier, group in movie_files.iteritems():
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']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)]
@@ -231,19 +232,53 @@ class Scanner(Plugin):
# Group the files based on the identifier
for identifier, group in movie_files.iteritems():
log.debug('Grouping files on identifier: %s' % identifier)
delete_identifiers = []
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['unsorted_files'].extend(found_files)
group = movie_files.get(identifier)
if group:
group['unsorted_files'].extend(found_files)
delete_identifiers.append(identifier)
# Remove the found files from the leftover stack
leftovers = leftovers - found_files
# Remove the found files from the leftover stack
leftovers = leftovers - set(found_files)
# 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
# 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
processed_movies = {}
@@ -256,13 +291,38 @@ class Scanner(Plugin):
# Check if movie is fresh and maybe still unpacking, ignore files new then 1 minute
file_too_new = False
for cur_file in group['unsorted_files']:
file_time = os.path.getmtime(cur_file)
if file_time > time.time() - 60:
file_too_new = tryInt(time.time() - file_time)
if not os.path.isfile(cur_file):
file_too_new = time.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
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
# Group extra (and easy) files first
@@ -284,10 +344,10 @@ class Scanner(Plugin):
group['files']['movie'] = self.getMediaFiles(group['unsorted_files'])
if len(group['files']['movie']) == 0:
log.error('Couldn\t find any movie files for %s' % identifier)
log.error('Couldn\t find any movie files for %s', identifier)
continue
log.debug('Getting metadata for %s' % identifier)
log.debug('Getting metadata for %s', identifier)
group['meta_data'] = self.getMetaData(group)
# Subtitle meta
@@ -320,7 +380,11 @@ class Scanner(Plugin):
# Determine movie
group['library'] = self.determineMovie(group)
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
@@ -329,9 +393,9 @@ class Scanner(Plugin):
self.path_identifiers = {}
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:
log.debug('Found no movies in the folder %s' % (folder))
log.debug('Found no movies in the folder %s', (folder))
return processed_movies
def getMetaData(self, group):
@@ -351,7 +415,7 @@ class Scanner(Plugin):
data['resolution_height'] = meta.get('resolution_height', 480)
data['aspect'] = meta.get('resolution_width', 720) / meta.get('resolution_height', 480)
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
if data.get('audio'): break
@@ -379,11 +443,11 @@ class Scanner(Plugin):
'resolution_height': tryInt(p.video[0].height),
}
except ParseError:
log.debug('Failed to parse meta for %s' % filename)
log.debug('Failed to parse meta for %s', filename)
except NoParserError:
log.debug('No parser found for %s' % filename)
log.debug('No parser found for %s', filename)
except:
log.debug('Failed parsing %s' % filename)
log.debug('Failed parsing %s', filename)
return {}
@@ -396,7 +460,7 @@ class Scanner(Plugin):
scan_result = []
for p in paths:
if not group['is_dvd']:
video = Video.from_path(p)
video = Video.from_path(toUnicode(p))
video_result = [(video, video.scan())]
scan_result.extend(video_result)
@@ -405,7 +469,7 @@ class Scanner(Plugin):
if s.language and s.path not in paths:
detected_languages[s.path] = [s.language]
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
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):
detected_languages[sub_file] = idx_langs
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
@@ -434,7 +498,7 @@ class Scanner(Plugin):
for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file)
if imdb_id:
log.debug('Found movie via CP tag: %s' % cur_file)
log.debug('Found movie via CP tag: %s', cur_file)
break
# Check and see if nfo contains the imdb-id
@@ -443,11 +507,23 @@ class Scanner(Plugin):
for nfo_file in files['nfo']:
imdb_id = getImdb(nfo_file)
if imdb_id:
log.debug('Found movie via nfo file: %s' % nfo_file)
log.debug('Found movie via nfo file: %s', nfo_file)
break
except:
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
if not imdb_id:
db = get_session()
@@ -455,7 +531,7 @@ class Scanner(Plugin):
f = db.query(File).filter_by(path = toUnicode(cur_file)).first()
try:
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
except:
pass
@@ -469,7 +545,7 @@ class Scanner(Plugin):
if len(movie) > 0:
imdb_id = movie[0]['imdb']
if imdb_id:
log.debug('Found movie via OpenSubtitleHash: %s' % cur_file)
log.debug('Found movie via OpenSubtitleHash: %s', cur_file)
break
# Search based on identifiers
@@ -486,17 +562,17 @@ class Scanner(Plugin):
if len(movie) > 0:
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
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:
return fireEvent('library.add', attrs = {
'identifier': imdb_id
}, 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 {}
def getCPImdb(self, string):
@@ -585,17 +661,17 @@ class Scanner(Plugin):
# ignoredpaths
for i in self.ignored_in_path:
if i in filename.lower():
log.debug('Ignored "%s" contains "%s".' % (filename, i))
log.debug('Ignored "%s" contains "%s".', (filename, i))
return False
# Sample file
if self.isSampleFile(filename):
log.debug('Is sample file "%s".' % filename)
log.debug('Is sample file "%s".', filename)
return False
# Minimal size
if self.filesizeBetween(filename, self.minimal_filesize['media']):
log.debug('File to small: %s' % filename)
log.debug('File to small: %s', filename)
return False
# All is OK
@@ -603,14 +679,14 @@ class Scanner(Plugin):
def isSampleFile(self, filename):
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
def filesizeBetween(self, file, min = 0, max = 100000):
try:
return (min * 1048576) < os.path.getsize(file) < (max * 1048576)
except:
log.error('Couldn\'t get filesize of %s.' % file)
log.error('Couldn\'t get filesize of %s.', file)
return False
@@ -629,7 +705,7 @@ class Scanner(Plugin):
identifier = self.removeCPTag(identifier)
# 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 = self.findYear(identifier)
@@ -703,26 +779,28 @@ class Scanner(Plugin):
def getReleaseNameYear(self, release_name, file_name = None):
# Use guessit first
guess = {}
if file_name:
try:
guess = guess_movie_info(file_name)
guess = guess_movie_info(toUnicode(file_name))
if guess.get('title') and guess.get('year'):
return {
guess = {
'name': guess.get('title'),
'year': guess.get('year'),
}
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
cleaned = ' '.join(re.split('\W+', simplifyString(release_name)))
cleaned = re.sub(self.clean, ' ', cleaned)
year = self.findYear(cleaned)
cp_guess = {}
if year: # Split name on year
try:
movie_name = cleaned.split(year).pop(0).strip()
return {
cp_guess = {
'name': movie_name,
'year': int(year),
}
@@ -731,11 +809,16 @@ class Scanner(Plugin):
else: # Split name on multiple spaces
try:
movie_name = cleaned.split(' ').pop(0).strip()
return {
cp_guess = {
'name': movie_name,
'year': int(year),
}
except:
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.plugins.base import Plugin
from couchpotato.core.plugins.score.scores import nameScore, nameRatioScore, \
sizeScore, providerScore, duplicateScore
sizeScore, providerScore, duplicateScore, partialIgnoredScore, namePositionScore, \
halfMultipartScore
log = CPLog(__name__)
@@ -21,6 +22,7 @@ class Score(Plugin):
for movie_title in movie['library']['titles']:
score += nameRatioScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
score += namePositionScore(toUnicode(nzb['name']), toUnicode(movie_title['title']))
score += sizeScore(nzb['size'])
@@ -38,4 +40,15 @@ class Score(Plugin):
# Duplicates in name
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
+87 -4
View File
@@ -1,5 +1,6 @@
from couchpotato.core.event import fireEvent
from couchpotato.core.helpers.encoding import simplifyString
from couchpotato.core.helpers.variable import tryInt
from couchpotato.environment import Env
import re
@@ -16,11 +17,12 @@ name_scores = [
'german:-10', 'french:-10', 'spanish:-10', 'swesub:-20', 'danish:-10', 'dutch:-10',
# Release groups
'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:-40', 'trilogy:-40',
]
def nameScore(name, year):
''' Calculate score for words in the NZB name '''
@@ -47,8 +49,8 @@ def nameScore(name, year):
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))
movie_words = re.split('\W+', simplifyString(movie_name))
@@ -56,15 +58,68 @@ def nameRatioScore(nzb_name, movie_name):
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):
return 0 if size else -20
def providerScore(provider):
if provider in ['NZBMatrix', 'Nzbs', 'Newzbin']:
return 30
return 20
if provider in ['Newznab', 'Moovee', 'X264']:
if provider in ['Newznab']:
return 10
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]
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',
'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.settings.model import Movie, Release, ReleaseInfo
from couchpotato.environment import Env
from inspect import ismethod, isfunction
from sqlalchemy.exc import InterfaceError
import datetime
import re
@@ -28,6 +29,9 @@ class Searcher(Plugin):
# 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'))
addEvent('app.load', self.all_movies)
def all_movies(self):
if self.in_progress:
@@ -55,7 +59,7 @@ class Searcher(Plugin):
except IndexError:
fireEvent('library.update', movie_dict['library']['identifier'], force = True)
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
if self.shuttingDown():
@@ -77,36 +81,46 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
default_title = getTitle(movie['library'])
if not default_title:
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']:
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
has_better_quality = 0
# See if beter quality is available
# See if better quality is available
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
# Don't search for quality lower then already available.
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)
results = fireEvent('yarr.search', movie, quality, merge = True)
sorted_results = sorted(results, key = lambda k: k['score'], reverse = True)
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
if not db.query(Movie).filter_by(id = movie.get('id')).first():
return
break
# Add them to this movie releases list
for nzb in sorted_results:
@@ -137,31 +151,52 @@ class Searcher(Plugin):
rls.info.append(rls_info)
db.commit()
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:
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)
if downloaded:
return True
else:
if downloaded is True:
ret = True
break
elif downloaded != 'try_next':
break
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'])
break
# Break if CP wants to shut down
if self.shuttingDown():
if self.shuttingDown() or ret:
break
fireEvent('notify.frontend', type = 'searcher.ended.%s' % movie['id'], data = True)
#db.close()
return False
return ret
def download(self, data, movie, manual = False):
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:
@@ -185,7 +220,7 @@ class Searcher(Plugin):
if movie['status_id'] == active_status.get('id'):
for profile_type in movie['profile']['types']:
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
rls.status_id = done_status.get('id')
@@ -196,7 +231,7 @@ class Searcher(Plugin):
mvie.status_id = done_status.get('id')
db.commit()
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()
return True
@@ -211,27 +246,29 @@ class Searcher(Plugin):
retention = Env.setting('retention', section = 'nzb')
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
movie_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', movie_name)
required_words = [x.strip() for x in self.conf('required_words').split(',')]
movie_name = getTitle(movie['library'])
movie_words = re.split('\W+', simplifyString(movie_name))
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)):
log.info("NZB doesn't contain any of the required words.")
log.info("Wrong: Required word missing: %s" % nzb['name'])
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))
if self.conf('ignored_words') and blacklisted:
log.info("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
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:
if p_tag in movie_name:
log.info('Wrong: %s, probably pr0n' % (nzb['name']))
if p_tag in nzb_words and p_tag not in movie_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
#qualities = fireEvent('quality.all', single = True)
@@ -239,18 +276,28 @@ class Searcher(Plugin):
# Contains lower quality string
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
# File to small
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
# File to large
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
@@ -277,7 +324,7 @@ class Searcher(Plugin):
if self.checkNFO(nzb['name'], movie['library']['identifier']):
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
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:
return True
else:
# 6 weeks after theater release
if dates.get('theater') + 3628800 < now:
# 12 weeks after theater release
if dates.get('theater') > 0 and dates.get('theater') + 7257600 < now:
return True
# 6 weeks before dvd release
if dates.get('dvd') - 3628800 < now:
return True
if dates.get('dvd') > 0:
# Dvd should be released
if dates.get('dvd') > 0 and dates.get('dvd') < now:
return True
# 3 weeks before dvd release
if dates.get('dvd') - 1814400 < now:
return True
# Dvd should be released
if dates.get('dvd') < now:
return True
return False
+1 -1
View File
@@ -92,7 +92,7 @@ class StatusPlugin(Plugin):
for identifier, label in self.statuses.iteritems():
s = db.query(Status).filter_by(identifier = identifier).first()
if not s:
log.info('Creating status: %s' % label)
log.info('Creating status: %s', label)
s = Status(
identifier = identifier,
label = toUnicode(label)
+21 -9
View File
@@ -1,10 +1,12 @@
from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, FileType
from couchpotato.environment import Env
import subliminal
import traceback
log = CPLog(__name__)
@@ -44,16 +46,26 @@ class Subtitle(Plugin):
if self.isDisabled(): return
available_languages = sum(group['subtitle_language'].itervalues(), [])
downloaded = []
for lang in self.getLanguages():
if lang not in available_languages:
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)
try:
available_languages = sum(group['subtitle_language'].itervalues(), [])
downloaded = []
files = [toUnicode(x) for x in group['files']['movie']]
for d_sub in downloaded:
group['files']['subtitle'].add(d_sub.path)
group['subtitle_language'][d_sub.path] = [d_sub.language]
for lang in self.getLanguages():
if lang not in available_languages:
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):
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.helpers.variable import getExt
from couchpotato.core.helpers.variable import getExt, getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
import os
@@ -17,13 +17,16 @@ class Trailer(Plugin):
if self.isDisabled() or len(group['files']['trailer']) > 0: return
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'), []):
destination = '%s-trailer.%s' % (self.getRootName(group), getExt(trailer))
if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
else:
log.debug('Trailer already exists: %s' % destination)
log.debug('Trailer already exists: %s', destination)
# Download first and break
break
+1 -1
View File
@@ -73,7 +73,7 @@ class Userscript(Plugin):
'movie': fireEvent('userscript.get_movie_via_url', url = url, single = True)
}
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'
return jsonified(params)
+15 -6
View File
@@ -20,7 +20,7 @@ class Automation(Plugin):
def _getMovies(self):
if not self.canCheck():
log.debug('Just checked, skipping %s' % self.getName())
log.debug('Just checked, skipping %s', self.getName())
return []
self.last_checked = time.time()
@@ -35,19 +35,28 @@ class Automation(Plugin):
else:
return None
def isMinimal(self, identifier):
movie = fireEvent('movie.info', identifier = identifier, merge = True)
def isMinimalMovie(self, movie):
if movie['rating'] and movie['rating'].get('imdb'):
movie['votes'] = movie['rating']['imdb'][1]
movie['rating'] = movie['rating']['imdb'][0]
identifier = movie['imdb']
for minimal_type in ['year', 'rating', 'votes']:
type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type)
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 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):
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',
'name': 'imdb_automation',
'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': [
{
'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.providers.automation.base import Automation
from couchpotato.environment import Env
from dateutil.parser import parse
import StringIO
import csv
import time
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
class IMDB(Automation):
class IMDB(Automation, RSS):
interval = 1800
@@ -21,34 +21,45 @@ class IMDB(Automation):
return
movies = []
headers = {}
for csv_url in self.conf('automation_urls').split(','):
prop_name = 'automation.imdb.last_update.%s' % md5(csv_url)
enablers = self.conf('automation_urls_use').split(',')
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_movie_added = 0
try:
cache_key = 'imdb_csv.%s' % md5(csv_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
cache_key = 'imdb.rss.%s' % md5(rss_url)
for row in csv_reader:
created = int(time.mktime(parse(row[headers['created']]).timetuple()))
if created < last_update:
rss_data = self.getCache(cache_key, rss_url)
data = XMLTree.fromstring(rss_data)
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
imdb = row[headers['const']]
if imdb:
movies.append(imdb)
movies.append(imdb)
except:
log.error('Failed loading IMDB watchlist: %s %s' % (csv_url, traceback.format_exc()))
Env.prop(prop_name, time.time())
log.error('Failed loading IMDB watchlist: %s %s', (rss_url, traceback.format_exc()))
Env.prop(prop_name, last_movie_added)
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):
if self.conf('automation_password'):
headers = {
'Authorization': "Basic %s" % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
}
else:
headers = {}
try:
if self.conf('automation_password'):
headers = {
'Authorization': 'Basic %s' % base64.encodestring('%s:%s' % (self.conf('automation_username'), self.conf('automation_password')))[:-1]
}
else:
headers = {}
cache_key = 'trakt.%s' % md5(method_url)
json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers)
return json.loads(json_string)
cache_key = 'trakt.%s' % md5(method_url)
json_string = self.getCache(cache_key, self.urls['base'] + method_url, headers = headers)
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.environment import Env
from urlparse import urlparse
from urllib import quote_plus
from couchpotato.core.helpers.encoding import simplifyString
import re
import time
@@ -31,7 +34,7 @@ class Provider(Plugin):
self.urlopen(test_url, 30)
self.is_available[host] = True
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
return self.is_available.get(host, False)
@@ -62,8 +65,11 @@ class YarrProvider(Provider):
def search(self, movie, quality):
return []
def belongsTo(self, url, host = None):
def belongsTo(self, url, provider = None, host = None):
try:
if provider and provider == self.getName():
return self
hostname = urlparse(url).hostname
if host and hostname in host:
return self
@@ -73,7 +79,7 @@ class YarrProvider(Provider):
if hostname in download_url:
return self
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
@@ -106,4 +112,4 @@ class YarrProvider(Provider):
return [self.cat_backup_id]
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):
if self.isDisabled(): return
log.info('Creating %s metadata.' % self.getName())
log.info('Creating %s metadata.', self.getName())
# Update library to get latest info
try:
updated_library = fireEvent('library.update', release['library']['identifier'], force = True, single = True)
release['library'] = mergeDicts(release['library'], updated_library)
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)
meta_name = os.path.basename(root_name)
@@ -44,14 +44,14 @@ class MetaDataBase(Plugin):
# Get file content
content = getattr(self, 'get' + file_type.capitalize())(movie_info = movie_info, data = release)
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):
shutil.copy2(content, name)
else:
self.createFile(name, content)
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):
return
@@ -75,7 +75,7 @@ class MetaDataBase(Plugin):
break
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')
def getFanart(self, movie_info = {}, data = {}):
@@ -77,7 +77,7 @@ class XBMC(MetaDataBase):
votes.text = str(v)
break
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
for genre in movie_info.get('genres', []):
@@ -1,10 +1,9 @@
from couchpotato import get_session
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.plugins.base import Plugin
from couchpotato.core.settings.model import Library
import time
import traceback
log = CPLog(__name__)
@@ -23,11 +22,19 @@ class MovieResultModifier(Plugin):
# Combine on imdb id
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):
temp[imdb] = self.getLibraryTags(imdb)
order.append(imdb)
if item.get('via_imdb'):
if order.index(imdb):
order.remove(imdb)
order.insert(0, imdb)
# Merge dicts
temp[imdb] = mergeDicts(temp[imdb], item)
@@ -61,7 +68,7 @@ class MovieResultModifier(Plugin):
if release.status_id == done_status['id']:
temp['in_library'] = fireEvent('movie.get', movie.id, single = True)
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()
return temp
@@ -32,10 +32,10 @@ class CouchPotatoApi(MovieProvider):
headers = {'X-CP-Version': fireEvent('app.version', single = True)}
data = self.urlopen((self.api_url % ('eta')) + (identifier + '/'), headers = headers)
dates = json.loads(data)
log.debug('Found ETA for %s: %s' % (identifier, dates))
log.debug('Found ETA for %s: %s', (identifier, dates))
return dates
except Exception, e:
log.error('Error getting ETA for %s: %s' % (identifier, e))
log.error('Error getting ETA for %s: %s', (identifier, e))
return {}
@@ -43,9 +43,9 @@ class CouchPotatoApi(MovieProvider):
try:
data = self.urlopen((self.api_url % ('suggest')) + ','.join(movies) + '/' + ','.join(ignore) + '/')
suggestions = json.loads(data)
log.info('Found Suggestions for %s' % (suggestions))
log.info('Found Suggestions for %s', (suggestions))
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
@@ -21,6 +21,7 @@ class IMDBAPI(MovieProvider):
def __init__(self):
addEvent('movie.search', self.search)
addEvent('movie.searchimdb', self.search)
addEvent('movie.info', self.getInfo)
def search(self, q, limit = 12):
@@ -36,7 +37,7 @@ class IMDBAPI(MovieProvider):
if cached:
result = self.parseMovie(cached)
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 []
@@ -54,7 +55,7 @@ class IMDBAPI(MovieProvider):
if cached:
result = self.parseMovie(cached)
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 {}
@@ -82,13 +83,14 @@ class IMDBAPI(MovieProvider):
year = tryInt(movie.get('Year', ''))
movie_data = {
'via_imdb': True,
'titles': [movie.get('Title')] if movie.get('Title') else [],
'original_title': movie.get('Title', ''),
'images': {
'poster': [movie.get('Poster', '')] if movie.get('Poster') and len(movie.get('Poster', '')) > 4 else [],
},
'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))),
},
'imdb': str(movie.get('imdbID', '')),
@@ -102,17 +104,17 @@ class IMDBAPI(MovieProvider):
'actors': movie.get('Actors', '').split(','),
}
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
def runtimeToMinutes(self, runtime_str):
runtime = 0
regex = '(\d*.?\d+).(hr|hrs|mins|min)+'
regex = '(\d*.?\d+).(h|hr|hrs|mins|min)+'
matches = re.findall(regex, runtime_str)
for match in matches:
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
@@ -3,7 +3,6 @@ from couchpotato.core.helpers.encoding import simplifyString, toUnicode
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
from libs.themoviedb import tmdb
import re
log = CPLog(__name__)
@@ -29,7 +28,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key)
if not results:
log.debug('Searching for movie by hash: %s' % file)
log.debug('Searching for movie by hash: %s', file)
try:
raw = tmdb.searchByHashingFile(file)
@@ -37,15 +36,15 @@ class TheMovieDb(MovieProvider):
if raw:
try:
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)
return results
except SyntaxError, e:
log.error('Failed to parse XML response: %s' % e)
log.error('Failed to parse XML response: %s', e)
return False
except:
log.debug('No movies known by hash for: %s' % file)
log.debug('No movies known by hash for: %s', file)
pass
return results
@@ -61,7 +60,7 @@ class TheMovieDb(MovieProvider):
results = self.getCache(cache_key)
if not results:
log.debug('Searching for movie: %s' % q)
log.debug('Searching for movie: %s', q)
raw = tmdb.search(search_string)
results = []
@@ -76,18 +75,21 @@ class TheMovieDb(MovieProvider):
if nr == limit:
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)
return results
except SyntaxError, e:
log.error('Failed to parse XML response: %s' % e)
log.error('Failed to parse XML response: %s', e)
return False
return results
def getInfo(self, identifier = None):
if not identifier:
return {}
cache_key = 'tmdb.cache.%s' % identifier
result = self.getCache(cache_key)
@@ -96,7 +98,7 @@ class TheMovieDb(MovieProvider):
movie = None
try:
log.debug('Getting info: %s' % cache_key)
log.debug('Getting info: %s', cache_key)
movie = tmdb.imdbLookup(id = identifier)
except:
pass
@@ -117,7 +119,7 @@ class TheMovieDb(MovieProvider):
movie = None
try:
log.debug('Getting info: %s' % cache_key)
log.debug('Getting info: %s', cache_key)
movie = tmdb.getMovieInfo(id = id)
except:
pass
@@ -148,7 +150,8 @@ class TheMovieDb(MovieProvider):
year = None
movie_data = {
'id': int(movie.get('id', 0)),
'via_tmdb': True,
'tmdb_id': int(movie.get('id', 0)),
'titles': [toUnicode(movie.get('name'))],
'original_title': movie.get('original_name'),
'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.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.logger import CPLog
from couchpotato.core.providers.nzb.base import NZBProvider
@@ -22,11 +23,13 @@ class Mysterbin(NZBProvider):
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']):
if self.isDisabled():
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(','):
if len(q) + len(ignored.strip()) > 126:
break
q = '%s -%s' % (q, ignored.strip())
params = {
@@ -39,28 +42,28 @@ class Mysterbin(NZBProvider):
'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))
if data:
try:
html = BeautifulSoup(data)
resultable = html.find('table', attrs = {'class':'t'})
for result in resultable.findAll('tr'):
for result in resultable.find_all('tr'):
try:
myster_id = result.find('input', attrs = {'class': 'check4nzb'})['value']
# 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:
age = tryInt(temp.split(' ')[0])
break
# size
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():
size = self.parseSize(temp)
break
@@ -71,13 +74,14 @@ class Mysterbin(NZBProvider):
new = {
'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',
'provider': self.getName(),
'age': age,
'size': size,
'url': self.urls['download'] % myster_id,
'description': description,
'download': self.download,
'check_nzb': False,
}
@@ -39,7 +39,7 @@ class Newzbin(NZBProvider, RSS):
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']):
if self.isDisabled():
return results
format_id = self.getFormatId(type)
@@ -57,11 +57,12 @@ class Newzbin(NZBProvider, RSS):
'category': '6',
'ps_rb_video_format': str(cat_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)
cache_key = str('newzbin.%s.%s.%s' % (movie['library']['identifier'], str(format_id), str(cat_id)))
single_cat = True
data = self.getCache(cache_key)
if not data:
@@ -81,7 +82,7 @@ class Newzbin(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -115,12 +116,12 @@ class Newzbin(NZBProvider, RSS):
'description': self.getTextElement(nzb, "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 = True, single_category = single_cat, single = True)
imdb_results = True, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
@@ -132,7 +133,7 @@ class Newzbin(NZBProvider, RSS):
def download(self, url = '', nzb_id = ''):
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 = {
'username' : self.conf('username'),
@@ -140,7 +141,7 @@ class Newzbin(NZBProvider, RSS):
'reportid' : nzb_id
}, show_error = False)
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
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.environment import Env
from dateutil.parser import parse
from urllib2 import HTTPError
from urlparse import urlparse
import time
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -20,6 +23,8 @@ class Newznab(NZBProvider, RSS):
'search': 'movie',
}
limits_reached = {}
cat_ids = [
([2010], ['dvdr']),
([2030], ['cam', 'ts', 'dvdrip', 'tc', 'r5', 'scr']),
@@ -46,7 +51,7 @@ class Newznab(NZBProvider, RSS):
def singleFeed(self, host):
results = []
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
if self.isDisabled(host):
return results
arguments = tryUrlencode({
@@ -78,7 +83,7 @@ class Newznab(NZBProvider, RSS):
def singleSearch(self, host, movie, quality):
results = []
if self.isDisabled(host) or not self.isAvailable(self.getUrl(host['host'], self.urls['search'])):
if self.isDisabled(host):
return results
cat_id = self.getCatId(quality['identifier'])
@@ -107,7 +112,7 @@ class Newznab(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
log.debug('%s, %s', (self.getName(), e))
return results
results = []
@@ -121,8 +126,8 @@ class Newznab(NZBProvider, RSS):
elif item.attrib.get('name') == 'usenetdate':
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 size is 0: log.debug('Size 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")))
id = self.getTextElement(nzb, "guid").split('/')[-1:].pop()
new = {
@@ -139,13 +144,12 @@ class Newznab(NZBProvider, RSS):
}
if not for_feed:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
else:
@@ -153,7 +157,7 @@ class Newznab(NZBProvider, RSS):
return results
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
def getHosts(self):
@@ -172,17 +176,15 @@ class Newznab(NZBProvider, RSS):
return list
def belongsTo(self, url):
def belongsTo(self, url, provider = None):
hosts = self.getHosts()
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:
return result
return
def getUrl(self, host, type):
return cleanHost(host) + 'api?t=' + type
@@ -194,3 +196,27 @@ class Newznab(NZBProvider, RSS):
def getApiExt(self, host):
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.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.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog
@@ -19,15 +20,15 @@ class NZBClub(NZBProvider, RSS):
'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):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']):
if self.isDisabled():
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(','):
q = '%s -%s' % (q, ignored.strip())
@@ -40,7 +41,7 @@ class NZBClub(NZBProvider, RSS):
'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))
if data:
try:
@@ -48,7 +49,7 @@ class NZBClub(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -58,10 +59,15 @@ class NZBClub(NZBProvider, RSS):
size = enclosure['length']
date = self.getTextElement(nzb, "pubDate")
full_description = self.getCache('nzbclub.%s' % nzbclub_id, self.getTextElement(nzb, "link"), cache_timeout = 25920000)
html = BeautifulSoup(full_description)
nfo_pre = html.find('pre', attrs = {'class':'nfo'})
description = toUnicode(nfo_pre.text) if nfo_pre else ''
def extra_check(item):
full_description = self.getCache('nzbclub.%s' % nzbclub_id, item['detail_url'], cache_timeout = 25920000)
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 = {
'id': nzbclub_id,
@@ -73,19 +79,17 @@ class NZBClub(NZBProvider, RSS):
'url': enclosure['url'].replace(' ', '_'),
'download': self.download,
'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',
nzb = new, movie = movie, quality = quality,
imdb_results = False, single_category = False, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
@@ -94,3 +98,21 @@ class NZBClub(NZBProvider, RSS):
log.error('Failed to parse XML response from NZBClub')
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.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.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog
@@ -9,6 +10,7 @@ from couchpotato.environment import Env
from dateutil.parser import parse
import re
import time
import traceback
import xml.etree.ElementTree as XMLTree
log = CPLog(__name__)
@@ -26,10 +28,10 @@ class NzbIndex(NZBProvider, RSS):
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['api']):
if self.isDisabled():
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({
'q': q,
'age': Env.setting('retention', 'nzb'),
@@ -52,7 +54,7 @@ class NzbIndex(NZBProvider, RSS):
data = XMLTree.fromstring(data)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -63,42 +65,56 @@ class NzbIndex(NZBProvider, RSS):
try:
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:
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 = {
'id': nzbindex_id,
'type': 'nzb',
'provider': self.getName(),
'download': self.download,
'name': self.getTextElement(nzb, "title"),
'age': self.calculateAge(int(time.mktime(parse(self.getTextElement(nzb, "pubDate")).timetuple()))),
'size': tryInt(enclosure['length']) / 1024 / 1024,
'url': enclosure['url'],
'detail_url': enclosure['url'].replace('/download/', '/release/'),
'description': description,
'get_more_info': self.getMoreInfo,
'extra_check': extra_check,
'check_nzb': True,
}
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:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
return results
except SyntaxError:
log.error('Failed to parse XML response from NZBMatrix.com')
except:
log.error('Failed to parsing %s: %s', (self.getName(), traceback.format_exc()))
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):
return NZBProvider.isEnabled(self) and self.conf('enabled')
@@ -32,7 +32,7 @@ class NZBMatrix(NZBProvider, RSS):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['search']):
if self.isDisabled():
return results
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)
nzbs = self.getElements(data, 'channel/item')
except Exception, e:
log.debug('%s, %s' % (self.getName(), e))
log.debug('%s, %s', (self.getName(), e))
return results
for nzb in nzbs:
@@ -83,13 +83,13 @@ class NZBMatrix(NZBProvider, RSS):
'description': self.getTextElement(nzb, "description"),
'check_nzb': True,
}
new['score'] = fireEvent('score.calculate', new, movie, single = True)
is_correct_movie = fireEvent('searcher.correct_movie',
nzb = new, movie = movie, quality = quality,
imdb_results = True, single_category = single_cat, single = True)
if is_correct_movie:
new['score'] = fireEvent('score.calculate', new, movie, single = True)
results.append(new)
self.found(new)
@@ -99,6 +99,9 @@ class NZBMatrix(NZBProvider, RSS):
return results
def download(self, url = '', nzb_id = ''):
return self.urlopen(url, headers = {'User-Agent': Env.getIdentifier()})
def getApiExt(self):
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
import cookielib
import traceback
import urllib2
log = CPLog(__name__)
class TorrentProvider(YarrProvider):
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.helpers.variable import tryInt, getTitle
from couchpotato.core.logger import CPLog
@@ -34,7 +34,7 @@ class KickAssTorrents(TorrentProvider):
def search(self, movie, quality):
results = []
if self.isDisabled() or not self.isAvailable(self.urls['test']):
if self.isDisabled():
return results
cache_key = 'kickasstorrents.%s.%s' % (movie['library']['identifier'], quality.get('identifier'))
@@ -47,14 +47,14 @@ class KickAssTorrents(TorrentProvider):
try:
html = BeautifulSoup(data)
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:
continue
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'):
continue
@@ -68,15 +68,17 @@ class KickAssTorrents(TorrentProvider):
}
nr = 0
for td in temp.findAll('td'):
for td in temp.find_all('td'):
column_name = table_order[nr]
if column_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['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
elif column_name is 'size':
new['size'] = self.parseSize(td.text)
@@ -97,7 +99,7 @@ class KickAssTorrents(TorrentProvider):
results.append(new)
self.found(new)
except:
log.error('Failed parsing KickAssTorrents: %s' % traceback.format_exc())
log.error('Failed parsing KickAssTorrents: %s', traceback.format_exc())
except:
pass
@@ -1,16 +1,16 @@
from .main import Moovee
from .main import PublicHD
def start():
return Moovee()
return PublicHD()
config = [{
'name': 'moovee',
'name': 'publichd',
'groups': [
{
'tab': 'searcher',
'subtab': 'providers',
'name': '#alt.binaries.moovee',
'description': 'SD movies only',
'name': 'PublicHD',
'description': 'Public Torrent site with only HD content.',
'options': [
{
'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