Browse Source

Merge branch 'refs/heads/master' into desktop

Conflicts:
	version.py
tags/build/2.0.8
Ruud 12 years ago
parent
commit
2b8dfed475
  1. 13
      couchpotato/api.py
  2. 2
      couchpotato/core/_base/_core/__init__.py
  3. 6
      couchpotato/core/_base/_core/main.py
  4. 11
      couchpotato/core/_base/clientscript/main.py
  5. 46
      couchpotato/core/_base/scheduler/main.py
  6. 24
      couchpotato/core/_base/updater/main.py
  7. 4
      couchpotato/core/_base/updater/static/updater.js
  8. 39
      couchpotato/core/downloaders/base.py
  9. 12
      couchpotato/core/downloaders/nzbget/__init__.py
  10. 129
      couchpotato/core/downloaders/nzbget/main.py
  11. 11
      couchpotato/core/downloaders/nzbvortex/main.py
  12. 46
      couchpotato/core/downloaders/sabnzbd/main.py
  13. 11
      couchpotato/core/downloaders/transmission/__init__.py
  14. 124
      couchpotato/core/downloaders/transmission/main.py
  15. 49
      couchpotato/core/downloaders/utorrent/main.py
  16. 42
      couchpotato/core/event.py
  17. 18
      couchpotato/core/helpers/variable.py
  18. 20
      couchpotato/core/notifications/base.py
  19. 1
      couchpotato/core/notifications/boxcar/main.py
  20. 27
      couchpotato/core/notifications/core/main.py
  21. 66
      couchpotato/core/notifications/core/static/notification.js
  22. 2
      couchpotato/core/notifications/email/main.py
  23. 1
      couchpotato/core/notifications/growl/main.py
  24. 1
      couchpotato/core/notifications/notifo/main.py
  25. 1
      couchpotato/core/notifications/notifymyandroid/main.py
  26. 1
      couchpotato/core/notifications/notifymywp/main.py
  27. 1
      couchpotato/core/notifications/plex/main.py
  28. 1
      couchpotato/core/notifications/prowl/main.py
  29. 5
      couchpotato/core/notifications/pushalot/main.py
  30. 14
      couchpotato/core/notifications/pushover/main.py
  31. 1
      couchpotato/core/notifications/toasty/main.py
  32. 30
      couchpotato/core/notifications/trakt/__init__.py
  33. 46
      couchpotato/core/notifications/trakt/main.py
  34. 1
      couchpotato/core/notifications/twitter/main.py
  35. 8
      couchpotato/core/notifications/xbmc/main.py
  36. 14
      couchpotato/core/plugins/automation/__init__.py
  37. 7
      couchpotato/core/plugins/automation/main.py
  38. 14
      couchpotato/core/plugins/file/main.py
  39. 19
      couchpotato/core/plugins/log/static/log.css
  40. 2
      couchpotato/core/plugins/manage/__init__.py
  41. 2
      couchpotato/core/plugins/manage/main.py
  42. 18
      couchpotato/core/plugins/movie/main.py
  43. 78
      couchpotato/core/plugins/movie/static/list.js
  44. 98
      couchpotato/core/plugins/movie/static/movie.actions.js
  45. 366
      couchpotato/core/plugins/movie/static/movie.css
  46. 13
      couchpotato/core/plugins/movie/static/movie.js
  47. 224
      couchpotato/core/plugins/movie/static/search.css
  48. 71
      couchpotato/core/plugins/movie/static/search.js
  49. 20
      couchpotato/core/plugins/profile/main.py
  50. 39
      couchpotato/core/plugins/profile/static/profile.css
  51. 6
      couchpotato/core/plugins/quality/main.py
  52. 30
      couchpotato/core/plugins/release/main.py
  53. 11
      couchpotato/core/plugins/renamer/__init__.py
  54. 207
      couchpotato/core/plugins/renamer/main.py
  55. 53
      couchpotato/core/plugins/scanner/main.py
  56. 10
      couchpotato/core/plugins/score/scores.py
  57. 11
      couchpotato/core/plugins/searcher/__init__.py
  58. 47
      couchpotato/core/plugins/searcher/main.py
  59. 27
      couchpotato/core/plugins/status/main.py
  60. 2
      couchpotato/core/plugins/subtitle/__init__.py
  61. 4
      couchpotato/core/plugins/trailer/__init__.py
  62. 9
      couchpotato/core/plugins/trailer/main.py
  63. 24
      couchpotato/core/plugins/userscript/static/userscript.css
  64. 19
      couchpotato/core/plugins/userscript/static/userscript.js
  65. 5
      couchpotato/core/plugins/userscript/template.js
  66. 55
      couchpotato/core/plugins/wizard/static/wizard.css
  67. 37
      couchpotato/core/plugins/wizard/static/wizard.js
  68. 22
      couchpotato/core/providers/automation/base.py
  69. 34
      couchpotato/core/providers/automation/letterboxd/__init__.py
  70. 49
      couchpotato/core/providers/automation/letterboxd/main.py
  71. 1
      couchpotato/core/providers/base.py
  72. 3
      couchpotato/core/providers/movie/_modifier/main.py
  73. 25
      couchpotato/core/providers/movie/couchpotatoapi/main.py
  74. 8
      couchpotato/core/providers/nzb/binsearch/__init__.py
  75. 8
      couchpotato/core/providers/nzb/ftdworld/__init__.py
  76. 20
      couchpotato/core/providers/nzb/newznab/__init__.py
  77. 7
      couchpotato/core/providers/nzb/newznab/main.py
  78. 8
      couchpotato/core/providers/nzb/nzbclub/__init__.py
  79. 8
      couchpotato/core/providers/nzb/nzbindex/__init__.py
  80. 8
      couchpotato/core/providers/nzb/nzbsrus/__init__.py
  81. 8
      couchpotato/core/providers/nzb/nzbx/__init__.py
  82. 8
      couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py
  83. 8
      couchpotato/core/providers/torrent/iptorrents/__init__.py
  84. 8
      couchpotato/core/providers/torrent/kickasstorrents/__init__.py
  85. 8
      couchpotato/core/providers/torrent/passthepopcorn/__init__.py
  86. 8
      couchpotato/core/providers/torrent/publichd/__init__.py
  87. 8
      couchpotato/core/providers/torrent/sceneaccess/__init__.py
  88. 8
      couchpotato/core/providers/torrent/scenehd/__init__.py
  89. 8
      couchpotato/core/providers/torrent/thepiratebay/__init__.py
  90. 8
      couchpotato/core/providers/torrent/torrentday/__init__.py
  91. 8
      couchpotato/core/providers/torrent/torrentleech/__init__.py
  92. 3
      couchpotato/core/providers/torrent/torrentleech/main.py
  93. 6
      couchpotato/core/providers/userscript/criticker/__init__.py
  94. 6
      couchpotato/core/providers/userscript/criticker/main.py
  95. 3
      couchpotato/core/settings/__init__.py
  96. 26
      couchpotato/core/settings/model.py
  97. 8
      couchpotato/runner.py
  98. BIN
      couchpotato/static/fonts/Elusive-Icons.eot
  99. 298
      couchpotato/static/fonts/Elusive-Icons.svg
  100. BIN
      couchpotato/static/fonts/Elusive-Icons.ttf

13
couchpotato/api.py

@ -11,16 +11,12 @@ api_nonblock = {}
class NonBlockHandler(RequestHandler): class NonBlockHandler(RequestHandler):
def __init__(self, application, request, **kwargs): stoppers = []
cls = NonBlockHandler
cls.stoppers = []
super(NonBlockHandler, self).__init__(application, request, **kwargs)
@asynchronous @asynchronous
def get(self, route): def get(self, route):
cls = NonBlockHandler
start, stop = api_nonblock[route] start, stop = api_nonblock[route]
cls.stoppers.append(stop) self.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None)) start(self.onNewMessage, last_id = self.get_argument("last_id", None))
@ -30,12 +26,11 @@ class NonBlockHandler(RequestHandler):
self.finish(response) self.finish(response)
def on_connection_close(self): def on_connection_close(self):
cls = NonBlockHandler
for stop in cls.stoppers: for stop in self.stoppers:
stop(self.onNewMessage) stop(self.onNewMessage)
cls.stoppers = [] self.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs): def addApiView(route, func, static = False, docs = None, **kwargs):

2
couchpotato/core/_base/_core/__init__.py

@ -70,7 +70,7 @@ config = [{
'name': 'development', 'name': 'development',
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',
'description': 'Disables some checks/downloads for faster reloading.', 'description': 'Enable this if you\'re developing, and NOT in any other case, thanks.',
}, },
{ {
'name': 'data_dir', 'name': 'data_dir',

6
couchpotato/core/_base/_core/main.py

@ -79,7 +79,7 @@ class Core(Plugin):
def shutdown(): def shutdown():
self.initShutdown() self.initShutdown()
IOLoop.instance().add_callback(shutdown) IOLoop.current().add_callback(shutdown)
return 'shutdown' return 'shutdown'
@ -89,7 +89,7 @@ class Core(Plugin):
def restart(): def restart():
self.initShutdown(restart = True) self.initShutdown(restart = True)
IOLoop.instance().add_callback(restart) IOLoop.current().add_callback(restart)
return 'restarting' return 'restarting'
@ -128,7 +128,7 @@ class Core(Plugin):
log.debug('Save to shutdown/restart') log.debug('Save to shutdown/restart')
try: try:
IOLoop.instance().stop() IOLoop.current().stop()
except RuntimeError: except RuntimeError:
pass pass
except: except:

11
couchpotato/core/_base/clientscript/main.py

@ -1,10 +1,11 @@
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin from minify.jsmin import jsmin
import cssprefixer
import os import os
import traceback import traceback
@ -23,7 +24,6 @@ class ClientScript(Plugin):
'script': [ 'script': [
'scripts/library/mootools.js', 'scripts/library/mootools.js',
'scripts/library/mootools_more.js', 'scripts/library/mootools_more.js',
'scripts/library/prefix_free.js',
'scripts/library/uniform.js', 'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js', 'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js', 'scripts/library/form_replacement/form_radio.js',
@ -69,6 +69,7 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles) addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts) addEvent('clientscript.get_scripts', self.getScripts)
if not Env.get('dev'):
addEvent('app.load', self.minify) addEvent('app.load', self.minify)
self.addCore() self.addCore()
@ -108,8 +109,10 @@ class ClientScript(Plugin):
if file_type == 'script': if file_type == 'script':
data = jsmin(f) data = jsmin(f)
else: else:
data = cssmin(f) data = cssprefixer.process(f, debug = False, minify = True)
data = data.replace('../images/', '../static/images/') data = data.replace('../images/', '../static/images/')
data = data.replace('../fonts/', '../static/fonts/')
data = data.replace('../../static/', '../static/') # Replace inside plugins
raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data}) raw.append({'file': file_path, 'date': int(os.path.getmtime(file_path)), 'data': data})
@ -119,7 +122,7 @@ class ClientScript(Plugin):
data += self.comment.get(file_type) % (r.get('file'), r.get('date')) data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
data += r.get('data') + '\n\n' data += r.get('data') + '\n\n'
self.createFile(out, data.strip()) self.createFile(out, ss(data.strip()))
if not self.minified.get(file_type): if not self.minified.get(file_type):
self.minified[file_type] = {} self.minified[file_type] = {}

46
couchpotato/core/_base/scheduler/main.py

@ -16,52 +16,20 @@ class Scheduler(Plugin):
addEvent('schedule.cron', self.cron) addEvent('schedule.cron', self.cron)
addEvent('schedule.interval', self.interval) addEvent('schedule.interval', self.interval)
addEvent('schedule.start', self.start) addEvent('schedule.remove', self.remove)
addEvent('schedule.restart', self.start)
addEvent('app.load', self.start)
self.sched = Sched(misfire_grace_time = 60) self.sched = Sched(misfire_grace_time = 60)
self.sched.start()
self.started = True
def remove(self, identifier): def remove(self, identifier):
for type in ['interval', 'cron']: for cron_type in ['intervals', 'crons']:
try: try:
self.sched.unschedule_job(getattr(self, type)[identifier]['job']) self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
log.debug('%s unscheduled %s', (type.capitalize(), identifier)) log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
except: except:
pass pass
def start(self):
# Stop all running
self.stop()
# Crons
for identifier in self.crons:
try:
self.remove(identifier)
cron = self.crons[identifier]
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)
# Intervals
for identifier in self.intervals:
try:
self.remove(identifier)
interval = self.intervals[identifier]
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)
# Start it
log.debug('Starting scheduler')
self.sched.start()
self.started = True
log.debug('Scheduler started')
def doShutdown(self): def doShutdown(self):
super(Scheduler, self).doShutdown() super(Scheduler, self).doShutdown()
self.stop() self.stop()
@ -82,6 +50,7 @@ class Scheduler(Plugin):
'day': day, 'day': day,
'hour': hour, 'hour': hour,
'minute': minute, 'minute': minute,
'job': self.sched.add_cron_job(handle, day = day, hour = hour, minute = minute)
} }
def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0): def interval(self, identifier = '', handle = None, hours = 0, minutes = 0, seconds = 0):
@ -93,4 +62,5 @@ class Scheduler(Plugin):
'hours': hours, 'hours': hours,
'minutes': minutes, 'minutes': minutes,
'seconds': seconds, 'seconds': seconds,
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
} }

24
couchpotato/core/_base/updater/main.py

@ -15,6 +15,7 @@ import tarfile
import time import time
import traceback import traceback
import version import version
import zipfile
log = CPLog(__name__) log = CPLog(__name__)
@ -32,7 +33,6 @@ class Updater(Plugin):
else: else:
self.updater = SourceUpdater() self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.autoUpdate) addEvent('app.load', self.autoUpdate)
addEvent('updater.info', self.info) addEvent('updater.info', self.info)
@ -52,6 +52,15 @@ class Updater(Plugin):
'return': {'type': 'see updater.info'} 'return': {'type': 'see updater.info'}
}) })
addEvent('setting.save.updater.enabled.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.remove', 'updater.check', single = True)
if self.isEnabled():
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
self.autoUpdate() # Check after enabling
def autoUpdate(self): def autoUpdate(self):
if self.check() and self.conf('automatic') and not self.updater.update_failed: if self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate(): if self.updater.doUpdate():
@ -255,11 +264,11 @@ class SourceUpdater(BaseUpdater):
def doUpdate(self): def doUpdate(self):
try: try:
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch) download_data = fireEvent('cp.source_url', repo = self.repo_user, repo_name = self.repo_name, branch = self.branch, single = True)
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz') destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash')) + '.' + download_data.get('type')
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
destination = fireEvent('file.download', url = url, dest = destination, single = True) extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
destination = fireEvent('file.download', url = download_data.get('url'), dest = destination, single = True)
# Cleanup leftover from last time # Cleanup leftover from last time
if os.path.isdir(extracted_path): if os.path.isdir(extracted_path):
@ -267,9 +276,14 @@ class SourceUpdater(BaseUpdater):
self.makeDir(extracted_path) self.makeDir(extracted_path)
# Extract # Extract
if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path)
else:
tar = tarfile.open(destination) tar = tarfile.open(destination)
tar.extractall(path = extracted_path) tar.extractall(path = extracted_path)
tar.close() tar.close()
os.remove(destination) os.remove(destination)
if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])): if self.replaceWith(os.path.join(extracted_path, os.listdir(extracted_path)[0])):

4
couchpotato/core/_base/updater/static/updater.js

@ -5,7 +5,7 @@ var UpdaterBase = new Class({
initialize: function(){ initialize: function(){
var self = this; var self = this;
App.addEvent('load', self.info.bind(self, 1000)) App.addEvent('load', self.info.bind(self, 2000))
App.addEvent('unload', function(){ App.addEvent('unload', function(){
if(self.timer) if(self.timer)
clearTimeout(self.timer); clearTimeout(self.timer);
@ -84,7 +84,7 @@ var UpdaterBase = new Class({
'click': self.doUpdate.bind(self) 'click': self.doUpdate.bind(self)
} }
}) })
).inject($(document.body).getElement('.header')) ).inject(document.body)
}, },
doUpdate: function(){ doUpdate: function(){

39
couchpotato/core/downloaders/base.py

@ -1,5 +1,6 @@
from base64 import b32decode, b16encode from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider from couchpotato.core.providers.base import Provider
import random import random
@ -103,6 +104,12 @@ class Downloader(Provider):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash)) log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False return False
def downloadReturnId(self, download_id):
return {
'downloader': self.getName(),
'id': download_id
}
def isDisabled(self, manual, data): def isDisabled(self, manual, data):
return not self.isEnabled(manual, data) return not self.isEnabled(manual, data)
@ -116,3 +123,35 @@ class Downloader(Provider):
return super(Downloader, self).isEnabled() and \ return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \ ((d_manual and manual) or (d_manual is False)) and \
(not data or self.isCorrectType(data.get('type'))) (not data or self.isCorrectType(data.get('type')))
class StatusList(list):
provider = None
def __init__(self, provider, **kwargs):
self.provider = provider
self.kwargs = kwargs
super(StatusList, self).__init__()
def extend(self, results):
for r in results:
self.append(r)
def append(self, result):
new_result = self.fillResult(result)
super(StatusList, self).append(new_result)
def fillResult(self, result):
defaults = {
'id': 0,
'status': 'busy',
'downloader': self.provider.getName(),
'folder': '',
}
return mergeDicts(defaults, result)

12
couchpotato/core/downloaders/nzbget/__init__.py

@ -25,6 +25,12 @@ config = [{
'description': 'Hostname with port. Usually <strong>localhost:6789</strong>', 'description': 'Hostname with port. Usually <strong>localhost:6789</strong>',
}, },
{ {
'name': 'username',
'default': 'nzbget',
'advanced': True,
'description': 'Set a different username to connect. Default: nzbget',
},
{
'name': 'password', 'name': 'password',
'type': 'password', 'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>', 'description': 'Default NZBGet password is <i>tegbzn6789</i>',
@ -48,6 +54,12 @@ config = [{
'advanced': True, 'advanced': True,
'description': 'Disable this downloader for automated searches, but use it when I manually send a release.', 'description': 'Disable this downloader for automated searches, but use it when I manually send a release.',
}, },
{
'name': 'delete_failed',
'default': True,
'type': 'bool',
'description': 'Delete a release after the download has failed.',
},
], ],
} }
], ],

129
couchpotato/core/downloaders/nzbget/main.py

@ -1,9 +1,11 @@
from base64 import standard_b64encode from base64 import standard_b64encode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import ss from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from datetime import timedelta
import re import re
import shutil
import socket import socket
import traceback import traceback
import xmlrpclib import xmlrpclib
@ -14,7 +16,7 @@ class NZBGet(Downloader):
type = ['nzb'] type = ['nzb']
url = 'http://nzbget:%(password)s@%(host)s/xmlrpc' url = 'http://%(username)s:%(password)s@%(host)s/xmlrpc'
def download(self, data = {}, movie = {}, filedata = None): def download(self, data = {}, movie = {}, filedata = None):
@ -24,7 +26,7 @@ class NZBGet(Downloader):
log.info('Sending "%s" to NZBGet.', data.get('name')) log.info('Sending "%s" to NZBGet.', data.get('name'))
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')} url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
nzb_name = ss('%s.nzb' % self.createNzbName(data, movie)) nzb_name = ss('%s.nzb' % self.createNzbName(data, movie))
rpc = xmlrpclib.ServerProxy(url) rpc = xmlrpclib.ServerProxy(url)
@ -50,7 +52,124 @@ class NZBGet(Downloader):
if xml_response: if xml_response:
log.info('NZB sent successfully to NZBGet') log.info('NZB sent successfully to NZBGet')
return True nzb_id = md5(data['url']) # about as unique as they come ;)
couchpotato_id = "couchpotato=" + nzb_id
groups = rpc.listgroups()
file_id = [item['LastID'] for item in groups if item['NZBFilename'] == nzb_name]
confirmed = rpc.editqueue("GroupSetParameter", 0, couchpotato_id, file_id)
if confirmed:
log.debug('couchpotato parameter set in nzbget download')
return self.downloadReturnId(nzb_id)
else: else:
log.error('NZBGet could not add %s to the queue.', nzb_name) log.error('NZBGet could not add %s to the queue.', nzb_name)
return False return False
def getAllDownloadStatus(self):
log.debug('Checking NZBGet download status.')
url = self.url % {'host': self.conf('host'), 'username': self.conf('username'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to check status'):
log.info('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
# Get NZBGet data
try:
status = rpc.status()
groups = rpc.listgroups()
queue = rpc.postqueue(0)
history = rpc.history()
except:
log.error('Failed getting data: %s', traceback.format_exc(1))
return False
statuses = StatusList(self)
for item in groups:
log.debug('Found %s in NZBGet download queue', item['NZBFilename'])
try:
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
nzb_id = item['NZBID']
statuses.append({
'id': nzb_id,
'name': item['NZBFilename'],
'original_status': 'DOWNLOADING' if item['ActiveDownloads'] > 0 else 'QUEUED',
# Seems to have no native API function for time left. This will return the time left after NZBGet started downloading this item
'timeleft': str(timedelta(seconds = item['RemainingSizeMB'] / status['DownloadRate'] * 2 ^ 20)) if item['ActiveDownloads'] > 0 and not (status['DownloadPaused'] or status['Download2Paused']) else -1,
})
for item in queue: # 'Parameters' is not passed in rpc.postqueue
log.debug('Found %s in NZBGet postprocessing queue', item['NZBFilename'])
statuses.append({
'id': item['NZBID'],
'name': item['NZBFilename'],
'original_status': item['Stage'],
'timeleft': str(timedelta(seconds = 0)) if not status['PostPaused'] else -1,
})
for item in history:
log.debug('Found %s in NZBGet history. ParStatus: %s, ScriptStatus: %s, Log: %s', (item['NZBFilename'] , item['ParStatus'], item['ScriptStatus'] , item['Log']))
try:
nzb_id = [param['Value'] for param in item['Parameters'] if param['Name'] == 'couchpotato'][0]
except:
nzb_id = item['NZBID']
statuses.append({
'id': nzb_id,
'name': item['NZBFilename'],
'status': 'completed' if item['ParStatus'] == 'SUCCESS' and item['ScriptStatus'] == 'SUCCESS' else 'failed',
'original_status': item['ParStatus'] + ', ' + item['ScriptStatus'],
'timeleft': str(timedelta(seconds = 0)),
'folder': item['DestDir']
})
return statuses
def removeFailed(self, item):
log.info('%s failed downloading, deleting...', item['name'])
url = self.url % {'host': self.conf('host'), 'password': self.conf('password')}
rpc = xmlrpclib.ServerProxy(url)
try:
if rpc.writelog('INFO', 'CouchPotato connected to delete some history'):
log.info('Successfully connected to NZBGet')
else:
log.info('Successfully connected to NZBGet, but unable to send a message')
except socket.error:
log.error('NZBGet is not responding. Please ensure that NZBGet is running and host setting is correct.')
return False
except xmlrpclib.ProtocolError, e:
if e.errcode == 401:
log.error('Password is incorrect.')
else:
log.error('Protocol Error: %s', e)
return False
try:
history = rpc.history()
for hist in history:
if hist['Parameters'] and hist['Parameters']['couchpotato'] and hist['Parameters']['couchpotato'] == item['id']:
nzb_id = hist['ID']
path = hist['DestDir']
if rpc.editqueue('HistoryDelete', 0, "", [tryInt(nzb_id)]):
shutil.rmtree(path, True)
except:
log.error('Failed deleting: %s', traceback.format_exc(0))
return False
return True

11
couchpotato/core/downloaders/nzbvortex/main.py

@ -1,5 +1,5 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost from couchpotato.core.helpers.variable import cleanHost
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
@ -29,7 +29,9 @@ class NZBVortex(Downloader):
nzb_filename = self.createFileName(data, filedata, movie) nzb_filename = self.createFileName(data, filedata, movie)
self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True) self.call('nzb/add', params = {'file': (ss(nzb_filename), filedata)}, multipart = True)
return True raw_statuses = self.call('nzb')
nzb_id = [item['id'] for item in raw_statuses.get('nzbs', []) if item['name'] == nzb_filename][0]
return self.downloadReturnId(nzb_id)
except: except:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc()) log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False return False
@ -38,7 +40,7 @@ class NZBVortex(Downloader):
raw_statuses = self.call('nzb') raw_statuses = self.call('nzb')
statuses = [] statuses = StatusList(self)
for item in raw_statuses.get('nzbs', []): for item in raw_statuses.get('nzbs', []):
# Check status # Check status
@ -53,7 +55,8 @@ class NZBVortex(Downloader):
'name': item['uiTitle'], 'name': item['uiTitle'],
'status': status, 'status': status,
'original_status': item['state'], 'original_status': item['state'],
'timeleft':-1, 'timeleft': -1,
'folder': item['destinationPath'],
}) })
return statuses return statuses

46
couchpotato/core/downloaders/sabnzbd/main.py

@ -1,8 +1,9 @@
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import tryUrlencode, ss from couchpotato.core.helpers.encoding import tryUrlencode, ss
from couchpotato.core.helpers.variable import cleanHost, mergeDicts from couchpotato.core.helpers.variable import cleanHost, mergeDicts
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env from couchpotato.environment import Env
from datetime import timedelta
from urllib2 import URLError from urllib2 import URLError
import json import json
import traceback import traceback
@ -17,8 +18,7 @@ class Sabnzbd(Downloader):
log.info('Sending "%s" to SABnzbd.', data.get('name')) log.info('Sending "%s" to SABnzbd.', data.get('name'))
params = { req_params = {
'apikey': self.conf('api_key'),
'cat': self.conf('category'), 'cat': self.conf('category'),
'mode': 'addurl', 'mode': 'addurl',
'nzbname': self.createNzbName(data, movie), 'nzbname': self.createNzbName(data, movie),
@ -31,17 +31,15 @@ class Sabnzbd(Downloader):
# If it's a .rar, it adds the .rar extension, otherwise it stays .nzb # If it's a .rar, it adds the .rar extension, otherwise it stays .nzb
nzb_filename = self.createFileName(data, filedata, movie) nzb_filename = self.createFileName(data, filedata, movie)
params['mode'] = 'addfile' req_params['mode'] = 'addfile'
else: else:
params['name'] = data.get('url') req_params['name'] = data.get('url')
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(params)
try: try:
if params.get('mode') is 'addfile': if req_params.get('mode') is 'addfile':
sab = self.urlopen(url, timeout = 60, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) sab_data = self.call(req_params, params = {'nzbfile': (ss(nzb_filename), filedata)}, multipart = True)
else: else:
sab = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) sab_data = self.call(req_params)
except URLError: except URLError:
log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0)) log.error('Failed sending release, probably wrong HOST: %s', traceback.format_exc(0))
return False return False
@ -49,17 +47,15 @@ class Sabnzbd(Downloader):
log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0)) log.error('Failed sending release, use API key, NOT the NZB key: %s', traceback.format_exc(0))
return False return False
result = sab.strip() log.debug('Result from SAB: %s', sab_data)
if not result: if sab_data.get('status') and not sab_data.get('error'):
log.error('SABnzbd didn\'t return anything.')
return False
log.debug('Result text from SAB: %s', result[:40])
if result[:2] == 'ok':
log.info('NZB sent to SAB successfully.') log.info('NZB sent to SAB successfully.')
if filedata:
return self.downloadReturnId(sab_data.get('nzo_ids')[0])
else:
return True return True
else: else:
log.error(result[:40]) log.error('Error getting data from SABNZBd: %s', sab_data)
return False return False
def getAllDownloadStatus(self): def getAllDownloadStatus(self):
@ -85,14 +81,13 @@ class Sabnzbd(Downloader):
log.error('Failed getting history json: %s', traceback.format_exc(1)) log.error('Failed getting history json: %s', traceback.format_exc(1))
return False return False
statuses = [] statuses = StatusList(self)
# Get busy releases # Get busy releases
for item in queue.get('slots', []): for item in queue.get('slots', []):
statuses.append({ statuses.append({
'id': item['nzo_id'], 'id': item['nzo_id'],
'name': item['filename'], 'name': item['filename'],
'status': 'busy',
'original_status': item['status'], 'original_status': item['status'],
'timeleft': item['timeleft'] if not queue['paused'] else -1, 'timeleft': item['timeleft'] if not queue['paused'] else -1,
}) })
@ -111,7 +106,8 @@ class Sabnzbd(Downloader):
'name': item['name'], 'name': item['name'],
'status': status, 'status': status,
'original_status': item['status'], 'original_status': item['status'],
'timeleft': 0, 'timeleft': str(timedelta(seconds = 0)),
'folder': item['storage'],
}) })
return statuses return statuses
@ -133,21 +129,21 @@ class Sabnzbd(Downloader):
return True return True
def call(self, params, use_json = True): def call(self, request_params, use_json = True, **kwargs):
url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(params, { url = cleanHost(self.conf('host')) + 'api?' + tryUrlencode(mergeDicts(request_params, {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),
'output': 'json' 'output': 'json'
})) }))
data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}) data = self.urlopen(url, timeout = 60, show_error = False, headers = {'User-Agent': Env.getIdentifier()}, **kwargs)
if use_json: if use_json:
d = json.loads(data) d = json.loads(data)
if d.get('error'): if d.get('error'):
log.error('Error getting data from SABNZBd: %s', d.get('error')) log.error('Error getting data from SABNZBd: %s', d.get('error'))
return {} return {}
return d[params['mode']] return d.get(request_params['mode']) or d
else: else:
return data return data

11
couchpotato/core/downloaders/transmission/__init__.py

@ -41,16 +41,23 @@ config = [{
{ {
'name': 'directory', 'name': 'directory',
'type': 'directory', 'type': 'directory',
'description': 'Where should Transmission saved the downloaded files?', 'description': 'Download to this directory. Keep empty for default Transmission download directory.',
}, },
{ {
'name': 'ratio', 'name': 'ratio',
'default': 10, 'default': 10,
'type': 'int', 'type': 'float',
'advanced': True, 'advanced': True,
'description': 'Stop transfer when reaching ratio', 'description': 'Stop transfer when reaching ratio',
}, },
{ {
'name': 'ratiomode',
'default': 0,
'type': 'int',
'advanced': True,
'description': '0 = Use session limit, 1 = Use transfer limit, 2 = Disable limit.',
},
{
'name': 'manual', 'name': 'manual',
'default': 0, 'default': 0,
'type': 'bool', 'type': 'bool',

124
couchpotato/core/downloaders/transmission/main.py

@ -1,11 +1,15 @@
from base64 import b64encode from base64 import b64encode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt from couchpotato.core.helpers.encoding import isInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta
import httplib import httplib
import json import json
import os.path import os.path
import re import re
import shutil
import traceback
import urllib2 import urllib2
log = CPLog(__name__) log = CPLog(__name__)
@ -18,7 +22,7 @@ class Transmission(Downloader):
def download(self, data, movie, filedata = None): def download(self, data, movie, filedata = None):
log.debug('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type'))) log.info('Sending "%s" (%s) to Transmission.', (data.get('name'), data.get('type')))
# Load host from config and split out port. # Load host from config and split out port.
host = self.conf('host').split(':') host = self.conf('host').split(':')
@ -27,22 +31,24 @@ class Transmission(Downloader):
return False return False
# Set parameters for Transmission # Set parameters for Transmission
params = {
'paused': self.conf('paused', default = 0),
}
if len(self.conf('directory', default = '')) > 0:
folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1] folder_name = self.createFileName(data, filedata, movie)[:-len(data.get('type')) - 1]
folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep) folder_path = os.path.join(self.conf('directory', default = ''), folder_name).rstrip(os.path.sep)
# Create the empty folder to download too # Create the empty folder to download too
self.makeDir(folder_path) self.makeDir(folder_path)
params = { params['download-dir'] = folder_path
'paused': self.conf('paused', default = 0),
'download-dir': folder_path
}
torrent_params = {} torrent_params = {}
if self.conf('ratio'): if self.conf('ratio'):
torrent_params = { torrent_params = {
'seedRatioLimit': self.conf('ratio'), 'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': self.conf('ratio') 'seedRatioMode': self.conf('ratiomode')
} }
if not filedata and data.get('type') == 'torrent': if not filedata and data.get('type') == 'torrent':
@ -58,15 +64,97 @@ class Transmission(Downloader):
else: else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params) remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
if not remote_torrent:
return False
# Change settings of added torrents # Change settings of added torrents
if torrent_params: elif torrent_params:
trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params)
return True log.info('Torrent sent to Transmission successfully.')
return self.downloadReturnId(remote_torrent['torrent-added']['hashString'])
except:
log.error('Failed to change settings for transfer: %s', traceback.format_exc())
return False
def getAllDownloadStatus(self):
log.debug('Checking Transmission download status.')
# 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.')
return False
# Go through Queue
try:
trpc = TransmissionRPC(host[0], port = host[1], username = self.conf('username'), password = self.conf('password'))
return_params = {
'fields': ['id', 'name', 'hashString', 'percentDone', 'status', 'eta', 'isFinished', 'downloadDir', 'uploadRatio']
}
queue = trpc.get_alltorrents(return_params)
except Exception, err: except Exception, err:
log.error('Failed to change settings for transfer: %s', err) log.error('Failed getting queue: %s', err)
return False return False
statuses = StatusList(self)
# Get torrents status
# CouchPotato Status
#status = 'busy'
#status = 'failed'
#status = 'completed'
# Transmission Status
#status = 0 => "Torrent is stopped"
#status = 1 => "Queued to check files"
#status = 2 => "Checking files"
#status = 3 => "Queued to download"
#status = 4 => "Downloading"
#status = 4 => "Queued to seed"
#status = 6 => "Seeding"
#To do :
# add checking file
# manage no peer in a range time => fail
for item in queue['torrents']:
log.debug('name=%s / id=%s / downloadDir=%s / hashString=%s / percentDone=%s / status=%s / eta=%s / uploadRatio=%s / confRatio=%s / isFinished=%s', (item['name'], item['id'], item['downloadDir'], item['hashString'], item['percentDone'], item['status'], item['eta'], item['uploadRatio'], self.conf('ratio'), item['isFinished']))
if not os.path.isdir(Env.setting('from', 'renamer')):
log.error('Renamer "from" folder doesn\'t to exist.')
return
if (item['percentDone'] * 100) >= 100 and (item['status'] == 6 or item['status'] == 0) and item['uploadRatio'] > self.conf('ratio'):
try:
trpc.stop_torrent(item['hashString'], {})
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'completed',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)),
'folder': os.path.join(item['downloadDir'], item['name']),
})
except Exception, err:
log.error('Failed to stop and remove torrent "%s" with error: %s', (item['name'], err))
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'failed',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = 0)),
})
else:
statuses.append({
'id': item['hashString'],
'name': item['name'],
'status': 'busy',
'original_status': item['status'],
'timeleft': str(timedelta(seconds = item['eta'])), # Is ETA in seconds??
})
return statuses
class TransmissionRPC(object): class TransmissionRPC(object):
@ -97,6 +185,7 @@ class TransmissionRPC(object):
try: try:
open_request = urllib2.urlopen(request) open_request = urllib2.urlopen(request)
response = json.loads(open_request.read()) response = json.loads(open_request.read())
log.debug('request: %s', json.dumps(ojson))
log.debug('response: %s', json.dumps(response)) log.debug('response: %s', json.dumps(response))
if response['result'] == 'success': if response['result'] == 'success':
log.debug('Transmission action successfull') log.debug('Transmission action successfull')
@ -146,3 +235,18 @@ class TransmissionRPC(object):
arguments['ids'] = torrent_id arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag} post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
return self._request(post_data) return self._request(post_data)
def get_alltorrents(self, arguments):
post_data = {'arguments': arguments, 'method': 'torrent-get', 'tag': self.tag}
return self._request(post_data)
def stop_torrent(self, torrent_id, arguments):
arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-stop', 'tag': self.tag}
return self._request(post_data)
def remove_torrent(self, torrent_id, remove_local_data, arguments):
arguments['ids'] = torrent_id
arguments['delete-local-data'] = remove_local_data
post_data = {'arguments': arguments, 'method': 'torrent-remove', 'tag': self.tag}
return self._request(post_data)

49
couchpotato/core/downloaders/utorrent/main.py

@ -1,10 +1,12 @@
from base64 import b16encode, b32decode from base64 import b16encode, b32decode
from bencode import bencode, bdecode from bencode import bencode, bdecode
from couchpotato.core.downloaders.base import Downloader from couchpotato.core.downloaders.base import Downloader, StatusList
from couchpotato.core.helpers.encoding import isInt, ss from couchpotato.core.helpers.encoding import isInt, ss
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from hashlib import sha1 from hashlib import sha1
from multipartpost import MultipartPostHandler from multipartpost import MultipartPostHandler
from datetime import timedelta
import os
import cookielib import cookielib
import httplib import httplib
import json import json
@ -66,7 +68,7 @@ class uTorrent(Downloader):
self.utorrent_api.set_torrent(torrent_hash, torrent_params) self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0): if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash) self.utorrent_api.pause_torrent(torrent_hash)
return True return self.downloadReturnId(torrent_hash)
except Exception, err: except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err) log.error('Failed to send torrent to uTorrent: %s', err)
return False return False
@ -103,7 +105,36 @@ class uTorrent(Downloader):
log.debug('Nothing in queue') log.debug('Nothing in queue')
return False return False
statuses = [] statuses = StatusList(self)
download_folder = ''
settings_dict = {}
try:
data = self.utorrent_api.get_settings()
utorrent_settings = json.loads(data)
# Create settings dict
for item in utorrent_settings['settings']:
if item[1] == 0: # int
settings_dict[item[0]] = int(item[2] if not item[2].strip() == '' else '0')
elif item[1] == 1: # bool
settings_dict[item[0]] = True if item[2] == 'true' else False
elif item[1] == 2: # string
settings_dict[item[0]] = item[2]
log.debug('uTorrent settings: %s', settings_dict)
# Get the download path from the uTorrent settings
if settings_dict['dir_completed_download_flag']:
download_folder = settings_dict['dir_completed_download']
elif settings_dict['dir_active_download_flag']:
download_folder = settings_dict['dir_active_download']
else:
log.info('No download folder set in uTorrent. Please set a download folder')
return False
except Exception, err:
log.error('Failed to get settings from uTorrent: %s', err)
return False
# Get torrents # Get torrents
for item in queue.get('torrents', []): for item in queue.get('torrents', []):
@ -113,12 +144,18 @@ class uTorrent(Downloader):
if item[21] == 'Finished' or item[21] == 'Seeding': if item[21] == 'Finished' or item[21] == 'Seeding':
status = 'completed' status = 'completed'
if settings_dict['dir_add_label']:
release_folder = os.path.join(download_folder, item[11], item[2])
else:
release_folder = os.path.join(download_folder, item[2])
statuses.append({ statuses.append({
'id': item[0], 'id': item[0],
'name': item[2], 'name': item[2],
'status': status, 'status': status,
'original_status': item[1], 'original_status': item[1],
'timeleft': item[10], 'timeleft': str(timedelta(seconds = item[10])),
'folder': release_folder,
}) })
return statuses return statuses
@ -195,3 +232,7 @@ class uTorrentAPI(object):
def get_status(self): def get_status(self):
action = "list=1" action = "list=1"
return self._request(action) return self._request(action)
def get_settings(self):
action = "action=getsettings"
return self._request(action)

42
couchpotato/core/event.py

@ -16,10 +16,8 @@ def runHandler(name, handler, *args, **kwargs):
def addEvent(name, handler, priority = 100): def addEvent(name, handler, priority = 100):
if events.get(name): if not events.get(name):
e = events[name] events[name] = []
else:
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
def createHandle(*args, **kwargs): def createHandle(*args, **kwargs):
@ -35,7 +33,10 @@ def addEvent(name, handler, priority = 100):
return h return h
e.handle(createHandle, priority = priority) events[name].append({
'handler': createHandle,
'priority': priority,
})
def removeEvent(name, handler): def removeEvent(name, handler):
e = events[name] e = events[name]
@ -43,6 +44,12 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs): def fireEvent(name, *args, **kwargs):
if not events.get(name): return if not events.get(name): return
e = Event(name = name, threads = 10, asynch = kwargs.get('async', False), exc_info = True, traceback = True, lock = threading.RLock())
for event in events[name]:
e.handle(event['handler'], priority = event['priority'])
#log.debug('Firing event %s', name) #log.debug('Firing event %s', name)
try: try:
@ -52,6 +59,7 @@ def fireEvent(name, *args, **kwargs):
'single': False, # Return single handler 'single': False, # Return single handler
'merge': False, # Merge items 'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish 'in_order': False, # Fire them in specific order, waits for the other to finish
'async': False
} }
# Do options # Do options
@ -62,13 +70,6 @@ def fireEvent(name, *args, **kwargs):
options[x] = val options[x] = val
except: pass except: pass
e = events[name]
# Lock this event
e.lock.acquire()
e.asynchronous = False
# Make sure only 1 event is fired at a time when order is wanted # Make sure only 1 event is fired at a time when order is wanted
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single'] kwargs['event_return_on_result'] = options['single']
@ -76,9 +77,6 @@ def fireEvent(name, *args, **kwargs):
# Fire # Fire
result = e(*args, **kwargs) result = e(*args, **kwargs)
# Release lock for this event
e.lock.release()
if options['single'] and not options['merge']: if options['single'] and not options['merge']:
results = None results = None
@ -104,13 +102,14 @@ def fireEvent(name, *args, **kwargs):
# Merge # Merge
if options['merge'] and len(results) > 0: if options['merge'] and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict # Dict
if isinstance(results[0], dict): if isinstance(results[0], dict):
results.reverse()
merged = {} merged = {}
for result in results: for result in results:
merged = mergeDicts(merged, result) merged = mergeDicts(merged, result, prepend_list = True)
results = merged results = merged
# Lists # Lists
@ -140,13 +139,8 @@ def fireEvent(name, *args, **kwargs):
log.error('%s: %s', (name, traceback.format_exc())) log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs): def fireEventAsync(*args, **kwargs):
try: kwargs['async'] = True
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs) fireEvent(*args, **kwargs)
my_thread.setDaemon(True)
my_thread.start()
return True
except Exception, e:
log.error('%s: %s', (args[0], e))
def errorHandler(error): def errorHandler(error):
etype, value, tb = error etype, value, tb = error

18
couchpotato/core/helpers/variable.py

@ -10,6 +10,20 @@ import sys
log = CPLog(__name__) log = CPLog(__name__)
def link(src, dst):
if os.name == 'nt':
import ctypes
if ctypes.windll.kernel32.CreateHardLinkW(unicode(dst), unicode(src), 0) == 0: raise ctypes.WinError()
else:
os.link(src, dst)
def symlink(src, dst):
if os.name == 'nt':
import ctypes
if ctypes.windll.kernel32.CreateSymbolicLinkW(unicode(dst), unicode(src), 1 if os.path.isdir(src) else 0) in [0, 1280]: raise ctypes.WinError()
else:
os.symlink(src, dst)
def getUserDir(): def getUserDir():
try: try:
import pwd import pwd
@ -53,7 +67,7 @@ def getDataDir():
def isDict(object): def isDict(object):
return isinstance(object, dict) return isinstance(object, dict)
def mergeDicts(a, b): def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b) assert isDict(a), isDict(b)
dst = a.copy() dst = a.copy()
@ -67,7 +81,7 @@ def mergeDicts(a, b):
if isDict(current_src[key]) and isDict(current_dst[key]): if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key])) stack.append((current_dst[key], current_src[key]))
elif isinstance(current_src[key], list) and isinstance(current_dst[key], list): elif isinstance(current_src[key], list) and isinstance(current_dst[key], list):
current_dst[key].extend(current_src[key]) current_dst[key] = current_src[key] + current_dst[key] if prepend_list else current_dst[key] + current_src[key]
current_dst[key] = removeListDuplicates(current_dst[key]) current_dst[key] = removeListDuplicates(current_dst[key])
else: else:
current_dst[key] = current_src[key] current_dst[key] = current_src[key]

20
couchpotato/core/notifications/base.py

@ -2,13 +2,15 @@ from couchpotato.api import addApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.request import jsonified
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env from couchpotato.environment import Env
log = CPLog(__name__) log = CPLog(__name__)
class Notification(Plugin): class Notification(Provider):
type = 'notification'
default_title = Env.get('appname') default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!' test_message = 'ZOMG Lazors Pewpewpew!'
@ -16,11 +18,12 @@ class Notification(Plugin):
listen_to = [ listen_to = [
'renamer.after', 'movie.snatched', 'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated', 'updater.available', 'updater.updated',
'core.message',
] ]
dont_listen_to = [] dont_listen_to = []
def __init__(self): def __init__(self):
addEvent('notify.%s' % self.getName().lower(), self.notify) addEvent('notify.%s' % self.getName().lower(), self._notify)
addApiView(self.testNotifyName(), self.test) addApiView(self.testNotifyName(), self.test)
@ -33,10 +36,17 @@ class Notification(Plugin):
def notify(message = None, group = {}, data = None): def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched': if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
return return
return self.notify(message = message, data = data if data else group, listener = listener) return self._notify(message = message, data = data if data else group, listener = listener)
return notify return notify
def getNotificationImage(self, size = 'small'):
return 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/notify.couch.%s.png' % size
def _notify(self, *args, **kwargs):
if self.isEnabled():
return self.notify(*args, **kwargs)
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
pass pass
@ -46,7 +56,7 @@ class Notification(Plugin):
log.info('Sending test to %s', test_type) log.info('Sending test to %s', test_type)
success = self.notify( success = self._notify(
message = self.test_message, message = self.test_message,
data = {}, data = {},
listener = 'test' listener = 'test'

1
couchpotato/core/notifications/boxcar/main.py

@ -11,7 +11,6 @@ class Boxcar(Notification):
url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications' url = 'https://boxcar.io/devices/providers/7MNNXY3UIzVBwvzkKwkC/notifications'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
try: try:
message = message.strip() message = message.strip()

27
couchpotato/core/notifications/core/main.py

@ -1,12 +1,13 @@
from couchpotato import get_session from couchpotato import get_session
from couchpotato.api import addApiView, addNonBlockApiView from couchpotato.api import addApiView, addNonBlockApiView
from couchpotato.core.event import addEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import toUnicode from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParam from couchpotato.core.helpers.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif from couchpotato.core.settings.model import Notification as Notif
from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_ from sqlalchemy.sql.expression import or_
import threading import threading
import time import time
@ -21,11 +22,6 @@ class CoreNotifier(Notification):
messages = [] messages = []
listeners = [] listeners = []
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
def __init__(self): def __init__(self):
super(CoreNotifier, self).__init__() super(CoreNotifier, self).__init__()
@ -54,7 +50,10 @@ class CoreNotifier(Notification):
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener)) addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
addApiView('notification.listener', self.listener) addApiView('notification.listener', self.listener)
fireEvent('schedule.interval', 'core.check_messages', self.checkMessages, hours = 12, single = True)
addEvent('app.load', self.clean) addEvent('app.load', self.clean)
addEvent('app.load', self.checkMessages)
def clean(self): def clean(self):
@ -112,6 +111,22 @@ class CoreNotifier(Notification):
'notifications': notifications 'notifications': notifications
}) })
def checkMessages(self):
prop_name = 'messages.last_check'
last_check = tryInt(Env.prop(prop_name, default = 0))
messages = fireEvent('cp.messages', last_check = last_check, single = True)
for message in messages:
if message.get('time') > last_check:
fireEvent('core.message', message = message.get('message'), data = message)
if last_check < message.get('time'):
last_check = message.get('time')
Env.prop(prop_name, value = last_check)
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
db = get_session() db = get_session()

66
couchpotato/core/notifications/core/static/notification.js

@ -21,20 +21,17 @@ var NotificationBase = new Class({
App.addEvent('load', function(){ App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, { App.block.notification = new Block.Menu(self, {
'button_class': 'icon2.eye-open',
'class': 'notification_menu', 'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self) 'onOpen': self.markAsRead.bind(self)
}) })
$(App.block.notification).inject(App.getBlock('search'), 'after'); $(App.block.notification).inject(App.getBlock('search'), 'after');
self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide(); self.badge = new Element('div.badge').inject(App.block.notification, 'top').hide();
/* App.getBlock('notification').addLink(new Element('a.more', {
'href': App.createUrl('notifications'),
'text': 'Show older notifications'
})); */
}); });
window.addEvent('load', function(){ window.addEvent('load', function(){
self.startInterval.delay(Browser.safari ? 100 : 0, self) self.startInterval.delay($(window).getSize().x <= 480 ? 2000 : 300, self)
}); });
}, },
@ -47,14 +44,19 @@ var NotificationBase = new Class({
result.el = App.getBlock('notification').addLink( result.el = App.getBlock('notification').addLink(
new Element('span.'+(result.read ? 'read' : '' )).adopt( new Element('span.'+(result.read ? 'read' : '' )).adopt(
new Element('span.message', {'text': result.message}), new Element('span.message', {'html': result.message}),
new Element('span.added', {'text': added.timeDiffInWords(), 'title': added}) new Element('span.added', {'text': added.timeDiffInWords(), 'title': added})
) )
, 'top'); , 'top');
self.notifications.include(result); self.notifications.include(result);
if(!result.read) if(result.data.important !== undefined && !result.read){
var sticky = true
App.fireEvent('message', [result.message, sticky, result])
}
else if(!result.read){
self.setBadge(self.notifications.filter(function(n){ return !n.read}).length) self.setBadge(self.notifications.filter(function(n){ return !n.read}).length)
}
}, },
@ -64,20 +66,26 @@ var NotificationBase = new Class({
self.badge[value ? 'show' : 'hide']() self.badge[value ? 'show' : 'hide']()
}, },
markAsRead: function(){ markAsRead: function(force_ids){
var self = this; var self = this,
ids = force_ids;
if(!force_ids) {
var rn = self.notifications.filter(function(n){ var rn = self.notifications.filter(function(n){
return !n.read return !n.read && n.data.important === undefined
}) })
var ids = [] var ids = []
rn.each(function(n){ rn.each(function(n){
ids.include(n.id) ids.include(n.id)
}) })
}
if(ids.length > 0) if(ids.length > 0)
Api.request('notification.markread', { Api.request('notification.markread', {
'data': {
'ids': ids.join(',')
},
'onSuccess': function(){ 'onSuccess': function(){
self.setBadge('') self.setBadge('')
} }
@ -93,11 +101,20 @@ var NotificationBase = new Class({
return; return;
} }
Api.request('notification.listener', { self.request = Api.request('notification.listener', {
'data': {'init':true}, 'data': {'init':true},
'onSuccess': self.processData.bind(self) 'onSuccess': self.processData.bind(self)
}).send() }).send()
setInterval(function(){
if(self.request && self.request.isRunning()){
self.request.cancel();
self.startPoll()
}
}, 120000);
}, },
startPoll: function(){ startPoll: function(){
@ -143,26 +160,41 @@ var NotificationBase = new Class({
self.startPoll() self.startPoll()
}, },
showMessage: function(message){ showMessage: function(message, sticky, data){
var self = this; var self = this;
if(!self.message_container) if(!self.message_container)
self.message_container = new Element('div.messages').inject(document.body); self.message_container = new Element('div.messages').inject(document.body);
var new_message = new Element('div.message', { var new_message = new Element('div', {
'text': message 'class': 'message' + (sticky ? ' sticky' : ''),
}).inject(self.message_container); 'html': message
}).inject(self.message_container, 'top');
setTimeout(function(){ setTimeout(function(){
new_message.addClass('show') new_message.addClass('show')
}, 10); }, 10);
setTimeout(function(){ var hide_message = function(){
new_message.addClass('hide') new_message.addClass('hide')
setTimeout(function(){ setTimeout(function(){
new_message.destroy(); new_message.destroy();
}, 1000); }, 1000);
}, 4000); }
if(sticky)
new_message.grab(
new Element('a.close.icon2', {
'events': {
'click': function(){
self.markAsRead([data.id]);
hide_message();
}
}
})
);
else
setTimeout(hide_message, 4000);
}, },

2
couchpotato/core/notifications/email/main.py

@ -12,7 +12,6 @@ log = CPLog(__name__)
class Email(Notification): class Email(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
# Extract all the settings from settings # Extract all the settings from settings
from_address = self.conf('from') from_address = self.conf('from')
@ -50,6 +49,5 @@ class Email(Notification):
return True return True
except: except:
log.error('E-mail failed: %s', traceback.format_exc()) log.error('E-mail failed: %s', traceback.format_exc())
return False
return False return False

1
couchpotato/core/notifications/growl/main.py

@ -44,7 +44,6 @@ class Growl(Notification):
log.error('Failed register of growl: %s', traceback.format_exc()) log.error('Failed register of growl: %s', traceback.format_exc())
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
self.register() self.register()

1
couchpotato/core/notifications/notifo/main.py

@ -13,7 +13,6 @@ class Notifo(Notification):
url = 'https://api.notifo.com/v1/send_notification' url = 'https://api.notifo.com/v1/send_notification'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
try: try:
params = { params = {

1
couchpotato/core/notifications/notifymyandroid/main.py

@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyAndroid(Notification): class NotifyMyAndroid(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
nma = pynma.PyNMA() nma = pynma.PyNMA()
keys = splitString(self.conf('api_key')) keys = splitString(self.conf('api_key'))

1
couchpotato/core/notifications/notifymywp/main.py

@ -9,7 +9,6 @@ log = CPLog(__name__)
class NotifyMyWP(Notification): class NotifyMyWP(Notification):
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
keys = splitString(self.conf('api_key')) keys = splitString(self.conf('api_key'))
p = PyNMWP(keys, self.conf('dev_key')) p = PyNMWP(keys, self.conf('dev_key'))

1
couchpotato/core/notifications/plex/main.py

@ -46,7 +46,6 @@ class Plex(Notification):
return True return True
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")] hosts = [x.strip() + ':3000' for x in self.conf('host').split(",")]
successful = 0 successful = 0

1
couchpotato/core/notifications/prowl/main.py

@ -13,7 +13,6 @@ class Prowl(Notification):
} }
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = { data = {
'apikey': self.conf('api_key'), 'apikey': self.conf('api_key'),

5
couchpotato/core/notifications/pushalot/main.py

@ -12,16 +12,15 @@ class Pushalot(Notification):
} }
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = { data = {
'AuthorizationToken': self.conf('auth_token'), 'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title, 'Title': self.default_title,
'Body': toUnicode(message), 'Body': toUnicode(message),
'LinkTitle': toUnicode("CouchPotato"),
'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'), 'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'), 'IsSilent': self.conf('silent'),
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
'Source': toUnicode(self.default_title)
} }
headers = { headers = {

14
couchpotato/core/notifications/pushover/main.py

@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection from httplib import HTTPSConnection
@ -11,21 +12,26 @@ class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy' app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
http_handler = HTTPSConnection("api.pushover.net:443") http_handler = HTTPSConnection("api.pushover.net:443")
data = { api_data = {
'user': self.conf('user_key'), 'user': self.conf('user_key'),
'token': self.app_token, 'token': self.app_token,
'message': toUnicode(message), 'message': toUnicode(message),
'priority': self.conf('priority') 'priority': self.conf('priority'),
} }
if data and data.get('library'):
api_data.extend({
'url': toUnicode('http://www.imdb.com/title/%s/' % data['library']['identifier']),
'url_title': toUnicode('%s on IMDb' % getTitle(data['library'])),
})
http_handler.request('POST', http_handler.request('POST',
"/1/messages.json", "/1/messages.json",
headers = {'Content-type': 'application/x-www-form-urlencoded'}, headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data) body = tryUrlencode(api_data)
) )
response = http_handler.getresponse() response = http_handler.getresponse()

1
couchpotato/core/notifications/toasty/main.py

@ -12,7 +12,6 @@ class Toasty(Notification):
} }
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = { data = {
'title': self.default_title, 'title': self.default_title,

30
couchpotato/core/notifications/trakt/__init__.py

@ -0,0 +1,30 @@
from .main import Trakt
def start():
return Trakt()
config = [{
'name': 'trakt',
'groups': [
{
'tab': 'notifications',
'list': 'notification_providers',
'name': 'trakt',
'label': 'Trakt',
'description': 'add movies to your collection once downloaded. Fill in your username and password in the <a href="../automation/">Automation Trakt settings</a>',
'options': [
{
'name': 'notification_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'remove_watchlist_enabled',
'label': 'Remove from watchlist',
'default': False,
'type': 'bool',
},
],
}
],
}]

46
couchpotato/core/notifications/trakt/main.py

@ -0,0 +1,46 @@
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
log = CPLog(__name__)
class Trakt(Notification):
urls = {
'base': 'http://api.trakt.tv/%s',
'library': 'movie/library/%s',
'unwatchlist': 'movie/unwatchlist/%s',
}
listen_to = ['movie.downloaded']
def notify(self, message = '', data = {}, listener = None):
post_data = {
'username': self.conf('automation_username'),
'password' : self.conf('automation_password'),
'movies': [{
'imdb_id': data['library']['identifier'],
'title': data['library']['titles'][0]['title'],
'year': data['library']['year']
}] if data else []
}
result = self.call((self.urls['library'] % self.conf('automation_api_key')), post_data)
if self.conf('remove_watchlist_enabled'):
result = result and self.call((self.urls['unwatchlist'] % self.conf('automation_api_key')), post_data)
return result
def call(self, method_url, post_data):
try:
response = self.getJsonData(self.urls['base'] % method_url, params = post_data, cache_timeout = 1)
if response:
if response.get('status') == "success":
log.info('Successfully called Trakt')
return True
except:
pass
log.error('Failed to call trakt, check your login.')
return False

1
couchpotato/core/notifications/twitter/main.py

@ -32,7 +32,6 @@ class Twitter(Notification):
addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials) addApiView('notify.%s.credentials' % self.getName().lower(), self.getCredentials)
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret')) api = Api(self.consumer_key, self.consumer_secret, self.conf('access_token_key'), self.conf('access_token_secret'))

8
couchpotato/core/notifications/xbmc/main.py

@ -13,10 +13,8 @@ class XBMC(Notification):
listen_to = ['renamer.after'] listen_to = ['renamer.after']
use_json_notifications = {} use_json_notifications = {}
couch_logo_url = 'https://raw.github.com/RuudBurger/CouchPotatoServer/master/couchpotato/static/images/xbmc-notify.png'
def notify(self, message = '', data = {}, listener = None): def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'))
@ -28,7 +26,7 @@ class XBMC(Notification):
if self.use_json_notifications.get(host): if self.use_json_notifications.get(host):
response = self.request(host, [ response = self.request(host, [
('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.couch_logo_url}), ('GUI.ShowNotification', {'title': self.default_title, 'message': message, 'image': self.getNotificationImage('small')}),
('VideoLibrary.Scan', {}), ('VideoLibrary.Scan', {}),
]) ])
else: else:
@ -90,7 +88,7 @@ class XBMC(Notification):
self.use_json_notifications[host] = True self.use_json_notifications[host] = True
# send the text message # send the text message
resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image':self.couch_logo_url})]) resp = self.request(host, [('GUI.ShowNotification', {'title':self.default_title, 'message':message, 'image': self.getNotificationImage('small')})])
for result in resp: for result in resp:
if (result.get('result') and result['result'] == 'OK'): if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!') log.debug('Message delivered successfully!')
@ -113,7 +111,7 @@ class XBMC(Notification):
server = 'http://%s/xbmcCmds/' % host server = 'http://%s/xbmcCmds/' % host
# Notification(title, message [, timeout , image]) # Notification(title, message [, timeout , image])
cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.couch_logo_url)) cmd = "xbmcHttp?command=ExecBuiltIn(Notification(%s,%s,'',%s))" % (urllib.quote(data['title']), urllib.quote(data['message']), urllib.quote(self.getNotificationImage('medium')))
server += cmd server += cmd
# I have no idea what to set to, just tried text/plain and seems to be working :) # I have no idea what to set to, just tried text/plain and seems to be working :)

14
couchpotato/core/plugins/automation/__init__.py

@ -36,6 +36,20 @@ config = [{
'unit': 'hours', 'unit': 'hours',
'description': 'hours', 'description': 'hours',
}, },
{
'name': 'required_genres',
'label': 'Required Genres',
'default': '',
'placeholder': 'Example: Action, Crime & Drama',
'description': 'Ignore movies that don\'t contain at least one set of genres. Sets are separated by "," and each word within a set must be separated with "&"'
},
{
'name': 'ignored_genres',
'label': 'Ignored Genres',
'default': '',
'placeholder': 'Example: Horror, Comedy & Drama & Romance',
'description': 'Ignore movies that contain at least one set of genres. Sets work the same as above.'
},
], ],
}, },
], ],

7
couchpotato/core/plugins/automation/main.py

@ -10,11 +10,16 @@ class Automation(Plugin):
def __init__(self): def __init__(self):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12)) addEvent('app.load', self.setCrons)
if not Env.get('dev'): if not Env.get('dev'):
addEvent('app.load', self.addMovies) addEvent('app.load', self.addMovies)
addEvent('setting.save.automation.hour.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
def addMovies(self): def addMovies(self):
movies = fireEvent('automation.get_movies', merge = True) movies = fireEvent('automation.get_movies', merge = True)

14
couchpotato/core/plugins/file/main.py

@ -9,6 +9,8 @@ from couchpotato.core.plugins.base import Plugin
from couchpotato.core.plugins.scanner.main import Scanner from couchpotato.core.plugins.scanner.main import Scanner
from couchpotato.core.settings.model import FileType, File from couchpotato.core.settings.model import FileType, File
from couchpotato.environment import Env from couchpotato.environment import Env
from flask.helpers import send_file
from werkzeug.exceptions import NotFound
import os.path import os.path
import time import time
import traceback import traceback
@ -71,7 +73,7 @@ class FileManager(Plugin):
db = get_session() db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')): for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
for filename in walk_files: for filename in walk_files:
if root == python_cache or 'minified' in filename: continue if root == python_cache or 'minified' in filename or 'version' in filename: continue
file_path = os.path.join(root, filename) file_path = os.path.join(root, filename)
f = db.query(File).filter(File.path == toUnicode(file_path)).first() f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f: if not f:
@ -81,11 +83,13 @@ class FileManager(Plugin):
def showCacheFile(self, filename = ''): def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir') file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
filename = os.path.basename(filename)
from flask.helpers import send_from_directory if not os.path.isfile(file_path):
return send_from_directory(cache_dir, filename) log.error('File "%s" not found', file_path)
raise NotFound()
return send_file(file_path, conditional = True)
def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}): def download(self, url = '', dest = None, overwrite = False, urlopen_kwargs = {}):

19
couchpotato/core/plugins/log/static/log.css

@ -1,17 +1,18 @@
.page.log .nav { .page.log .nav {
display: block; display: block;
text-align: center; text-align: center;
padding: 20px 0; padding: 0 0 30px;
margin: 0; margin: 0;
font-size: 20px; font-size: 20px;
position: fixed; position: fixed;
width: 960px; width: 100%;
bottom: 0; bottom: 0;
left: 0;
background: #4E5969; background: #4E5969;
} }
.page.log .nav li { .page.log .nav li {
display: inline; display: inline-block;
padding: 5px 10px; padding: 5px 10px;
margin: 0; margin: 0;
cursor: pointer; cursor: pointer;
@ -24,7 +25,17 @@
.page.log .nav li.active { .page.log .nav li.active {
font-weight: bold; font-weight: bold;
cursor: default; cursor: default;
font-size: 30px; background: rgba(255,255,255,.1);
}
@media all and (max-width: 480px) {
.page.log .nav {
font-size: 14px;
}
.page.log .nav li {
padding: 5px;
}
} }
.page.log .loading { .page.log .loading {

2
couchpotato/core/plugins/manage/__init__.py

@ -8,7 +8,7 @@ config = [{
'groups': [ 'groups': [
{ {
'tab': 'manage', 'tab': 'manage',
'label': 'movie library manager', 'label': 'Movie Library Manager',
'description': 'Add your existing movie folders.', 'description': 'Add your existing movie folders.',
'options': [ 'options': [
{ {

2
couchpotato/core/plugins/manage/main.py

@ -185,6 +185,8 @@ class Manage(Plugin):
# Add it to release and update the info # Add it to release and update the info
fireEvent('release.add', group = group) fireEvent('release.add', group = group)
fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier)) fireEventAsync('library.update', identifier = identifier, on_complete = self.createAfterUpdate(folder, identifier))
else:
self.in_progress[folder]['to_go'] = self.in_progress[folder]['to_go'] - 1
return addToLibrary return addToLibrary

18
couchpotato/core/plugins/movie/main.py

@ -108,9 +108,8 @@ class MoviePlugin(Plugin):
now = time.time() now = time.time()
week = 262080 week = 262080
done_status = fireEvent('status.get', 'done', single = True) done_status, available_status, snatched_status = \
available_status = fireEvent('status.get', 'available', single = True) fireEvent('status.get', ['done', 'available', 'snatched'], single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
db = get_session() db = get_session()
@ -316,7 +315,7 @@ class MoviePlugin(Plugin):
for title in movie.library.titles: for title in movie.library.titles:
if title.default: default_title = title.title if title.default: default_title = title.title
fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True, message = 'Updating "%s"' % default_title) fireEvent('notify.frontend', type = 'movie.busy.%s' % id, data = True)
fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id)) fireEventAsync('library.update', identifier = movie.library.identifier, default_title = default_title, force = True, on_complete = self.createOnComplete(id))
@ -367,10 +366,8 @@ class MoviePlugin(Plugin):
library = fireEvent('library.add', single = True, attrs = params, update_after = update_library) library = fireEvent('library.add', single = True, attrs = params, update_after = update_library)
# Status # Status
status_active = fireEvent('status.add', 'active', single = True) status_active, snatched_status, ignored_status, done_status, downloaded_status = \
snatched_status = fireEvent('status.add', 'snatched', single = True) fireEvent('status.get', ['active', 'snatched', 'ignored', 'done', 'downloaded'], single = True)
ignored_status = fireEvent('status.add', 'ignored', single = True)
downloaded_status = fireEvent('status.add', 'downloaded', single = True)
default_profile = fireEvent('profile.default', single = True) default_profile = fireEvent('profile.default', single = True)
@ -397,7 +394,7 @@ class MoviePlugin(Plugin):
# Clean snatched history # Clean snatched history
for release in m.releases: for release in m.releases:
if release.status_id in [downloaded_status.get('id'), snatched_status.get('id')]: if release.status_id in [downloaded_status.get('id'), snatched_status.get('id'), done_status.get('id')]:
if params.get('ignore_previous', False): if params.get('ignore_previous', False):
release.status_id = ignored_status.get('id') release.status_id = ignored_status.get('id')
else: else:
@ -548,8 +545,7 @@ class MoviePlugin(Plugin):
def restatus(self, movie_id): def restatus(self, movie_id):
active_status = fireEvent('status.get', 'active', single = True) active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
done_status = fireEvent('status.get', 'done', single = True)
db = get_session() db = get_session()

78
couchpotato/core/plugins/movie/static/list.js

@ -1,13 +1,15 @@
var MovieList = new Class({ var MovieList = new Class({
Implements: [Options], Implements: [Events, Options],
options: { options: {
navigation: true, navigation: true,
limit: 50, limit: 50,
load_more: true, load_more: true,
loader: true,
menu: [], menu: [],
add_new: false add_new: false,
force_view: false
}, },
movies: [], movies: [],
@ -42,6 +44,9 @@ var MovieList = new Class({
}) : null }) : null
); );
if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details'); self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies(); self.getMovies();
@ -120,7 +125,7 @@ var MovieList = new Class({
if(!self.navigation_counter) return; if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0)); self.navigation_counter.set('text', (count || 0) + ' movies');
}, },
@ -144,12 +149,10 @@ var MovieList = new Class({
var self = this; var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.current_view = self.getSavedView() || 'details'; self.el.addClass('with_navigation')
self.el.addClass(self.current_view+'_list')
self.navigation = new Element('div.alph_nav').adopt( self.navigation = new Element('div.alph_nav').grab(
self.navigation_actions = new Element('ul.inlay.actions.reversed'), new Element('div').adopt(
self.navigation_counter = new Element('span.counter[title=Total]'),
self.navigation_alpha = new Element('ul.numbers', { self.navigation_alpha = new Element('ul.numbers', {
'events': { 'events': {
'click:relay(li)': function(e, el){ 'click:relay(li)': function(e, el){
@ -159,8 +162,11 @@ var MovieList = new Class({
} }
} }
}), }),
self.navigation_search_input = new Element('input.inlay', { self.navigation_counter = new Element('span.counter[title=Total]'),
'placeholder': 'Search', self.navigation_actions = new Element('ul.inlay.actions.reversed'),
self.navigation_search_input = new Element('input.search.inlay', {
'title': 'Search through ' + self.options.identifier,
'placeholder': 'Search through ' + self.options.identifier,
'events': { 'events': {
'keyup': self.search.bind(self), 'keyup': self.search.bind(self),
'change': self.search.bind(self) 'change': self.search.bind(self)
@ -205,6 +211,7 @@ var MovieList = new Class({
}) })
) )
) )
)
).inject(self.el, 'top'); ).inject(self.el, 'top');
// Mass edit // Mass edit
@ -247,11 +254,12 @@ var MovieList = new Class({
}); });
// Get available chars and highlight // Get available chars and highlight
if(self.navigation.isDisplayed() || self.navigation.isVisible())
Api.request('movie.available_chars', { Api.request('movie.available_chars', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status 'status': self.options.status
}, self.filter), }, self.filter),
'onComplete': function(json){ 'onSuccess': function(json){
json.chars.split('').each(function(c){ json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available') self.letters[c.capitalize()].addClass('available')
@ -266,17 +274,7 @@ var MovieList = new Class({
self.navigation_menu.addLink(menu_item); self.navigation_menu.addLink(menu_item);
}) })
else else
self.navigation_menu.hide() self.navigation_menu.hide();
self.nav_scrollspy = new ScrollSpy({
min: 10,
onEnter: function(){
self.navigation.addClass('float')
},
onLeave: function(){
self.navigation.removeClass('float')
}
});
}, },
@ -475,12 +473,39 @@ var MovieList = new Class({
self.load_more.set('text', 'loading...'); self.load_more.set('text', 'loading...');
} }
if(self.movies.length == 0 && self.options.loader){
self.loader_first = new Element('div.loading').adopt(
new Element('div.message', {'text': self.options.title ? 'Loading \'' + self.options.title + '\'' : 'Loading...'})
).inject(self.el, 'top');
createSpinner(self.loader_first, {
radius: 4,
length: 4,
width: 1
});
self.el.setStyle('min-height', 93);
}
Api.request(self.options.api_call || 'movie.list', { Api.request(self.options.api_call || 'movie.list', {
'data': Object.merge({ 'data': Object.merge({
'status': self.options.status, 'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset 'limit_offset': self.options.limit + ',' + self.offset
}, self.filter), }, self.filter),
'onComplete': function(json){ 'onSuccess': function(json){
if(self.loader_first){
var lf = self.loader_first;
self.loader_first.addClass('hide')
self.loader_first = null;
setTimeout(function(){
lf.destroy();
}, 20000);
self.el.setStyle('min-height', null);
}
self.store(json.movies); self.store(json.movies);
self.addMovies(json.movies, json.total); self.addMovies(json.movies, json.total);
if(self.scrollspy) { if(self.scrollspy) {
@ -488,7 +513,8 @@ var MovieList = new Class({
self.scrollspy.start(); self.scrollspy.start();
} }
self.checkIfEmpty() self.checkIfEmpty();
self.fireEvent('loaded');
} }
}); });
}, },
@ -515,10 +541,10 @@ var MovieList = new Class({
self.title[is_empty ? 'hide' : 'show']() self.title[is_empty ? 'hide' : 'show']()
if(self.description) if(self.description)
self.description[is_empty ? 'hide' : 'show']() self.description.setStyle('display', [is_empty ? 'none' : ''])
if(is_empty && self.options.on_empty_element){ if(is_empty && self.options.on_empty_element){
self.el.grab(self.options.on_empty_element); self.options.on_empty_element.inject(self.loader_first || self.title || self.movie_list, 'after');
if(self.navigation) if(self.navigation)
self.navigation.hide(); self.navigation.hide();

98
couchpotato/core/plugins/movie/static/movie.actions.js

@ -89,41 +89,22 @@ MA.Release = new Class({
} }
}); });
if(self.movie.data.releases.length == 0){ if(self.movie.data.releases.length == 0)
self.el.hide() self.el.hide()
} else
else { self.showHelper();
var buttons_done = false;
self.movie.data.releases.sortBy('-info.score').each(function(release){
if(buttons_done) return;
var status = Status.get(release.status_id);
if((self.next_release && (status.identifier == 'ignored' || status.identifier == 'failed')) || (!self.next_release && status.identifier == 'available')){
self.hide_on_click = false;
self.show();
buttons_done = true;
}
});
}
}, },
show: function(e){ createReleases: function(){
var self = this; var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){ if(!self.options_container){
self.options_container = new Element('div.options').adopt( self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt( self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container') self.trynext_container = new Element('div.buttons.try_container')
) )
).inject(self.movie, 'top'); );
// Header // Header
new Element('div.item.head').adopt( new Element('div.item.head').adopt(
@ -238,9 +219,71 @@ MA.Release = new Class({
} }
},
show: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.options_container.inject(self.movie, 'top');
self.movie.slide('in', self.options_container); self.movie.slide('in', self.options_container);
}, },
showHelper: function(e){
var self = this;
if(e)
(e).preventDefault();
self.createReleases();
self.trynext_container = new Element('div.buttons.trynext').inject(self.movie.info_container);
if(self.next_release || self.last_release){
self.trynext_container.adopt(
self.next_release ? [new Element('a.icon.readd', {
'text': self.last_release ? 'Download another release' : 'Download the best release',
'events': {
'click': self.tryNextRelease.bind(self)
}
}),
new Element('a.icon.download', {
'text': 'pick one yourself',
'events': {
'click': function(){
self.movie.quality.fireEvent('click');
}
}
})] : null,
new Element('a.icon.completed', {
'text': 'mark this movie done',
'events': {
'click': function(){
Api.request('movie.delete', {
'data': {
'id': self.movie.get('id'),
'delete_from': 'wanted'
},
'onComplete': function(){
var movie = $(self.movie);
movie.set('tween', {
'duration': 300,
'onComplete': function(){
self.movie.destroy()
}
});
movie.tween('height', 0);
}
});
}
}
})
)
}
},
get: function(release, type){ get: function(release, type){
return release.info[type] || 'n/a' return release.info[type] || 'n/a'
}, },
@ -251,14 +294,15 @@ MA.Release = new Class({
var release_el = self.release_container.getElement('#release_'+release.id), var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon'); icon = release_el.getElement('.download.icon');
icon.addClass('spinner'); self.movie.busy(true);
Api.request('release.download', { Api.request('release.download', {
'data': { 'data': {
'id': release.id 'id': release.id
}, },
'onComplete': function(json){ 'onComplete': function(json){
icon.removeClass('spinner') self.movie.busy(false);
if(json.success) if(json.success)
icon.addClass('completed'); icon.addClass('completed');
else else
@ -281,6 +325,8 @@ MA.Release = new Class({
tryNextRelease: function(movie_id){ tryNextRelease: function(movie_id){
var self = this; var self = this;
self.createReleases();
if(self.last_release) if(self.last_release)
self.ignore(self.last_release); self.ignore(self.last_release);

366
couchpotato/core/plugins/movie/static/movie.css

@ -1,25 +1,74 @@
.movies { .movies {
padding: 60px 0 20px; padding: 10px 0 20px;
position: relative; position: relative;
z-index: 3; z-index: 3;
width: 100%;
} }
.movies > div {
clear: both;
}
.movies.thumbs_list > div:not(.description) {
margin-right: -4px;
text-align: center;
}
.movies .loading {
display: block;
padding: 20px 0 0 0;
width: 100%;
z-index: 3;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
height: 40px;
opacity: 1;
position: absolute;
text-align: center;
}
.movies .loading.hide {
height: 0;
padding: 0;
opacity: 0;
margin-top: -20px;
overflow: hidden;
}
.movies .loading .spinner {
display: inline-block;
}
.movies .loading .message {
margin: 0 20px;
}
.movies h2 { .movies h2 {
margin-bottom: 20px; margin-bottom: 20px;
} }
@media all and (max-width: 480px) {
.movies h2 {
font-size: 25px;
margin-bottom: 10px;
}
}
.movies > .description { .movies > .description {
position: absolute; position: absolute;
top: 30px; top: 30px;
right: 0; right: 0;
font-style: italic; font-style: italic;
text-shadow: none;
opacity: 0.8; opacity: 0.8;
} }
.movies:hover > .description { .movies:hover > .description {
opacity: 1; opacity: 1;
} }
@media all and (max-width: 860px) {
.movies > .description {
display: none;
}
}
.movies.thumbs_list { .movies.thumbs_list {
padding: 20px 0 20px; padding: 20px 0 20px;
} }
@ -28,18 +77,19 @@
padding-top: 6px; padding-top: 6px;
} }
.movies.mass_edit_list {
padding-top: 90px;
}
.movies .movie { .movies .movie {
position: relative; position: relative;
border-radius: 4px; border-radius: 4px;
margin: 10px 0; margin: 10px 0;
padding-left: 20px;
overflow: hidden; overflow: hidden;
width: 100%; width: 100%;
height: 180px; height: 180px;
transition: all 0.2s linear; transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
}
.movies.details_list .movie {
padding-left: 120px;
} }
.movies.list_list .movie:not(.details_view), .movies.list_list .movie:not(.details_view),
@ -48,13 +98,21 @@
} }
.movies.thumbs_list .movie { .movies.thumbs_list .movie {
width: 153px; width: 16.66667%;
height: 230px; height: auto;
display: inline-block; display: inline-block;
margin: 0 8px 0 0;
}
.movies.thumbs_list .movie:nth-child(6n+6) {
margin: 0; margin: 0;
padding: 0;
vertical-align: top;
border-radius: 0;
box-shadow: none;
border: 0;
}
@media all and (max-width: 800px) {
.movies.thumbs_list .movie {
width: 25%;
}
} }
.movies .movie .mask { .movies .movie .mask {
@ -82,8 +140,8 @@
.movies .data { .movies .data {
padding: 20px; padding: 20px;
height: 100%; height: 100%;
width: 840px; width: 100%;
position: absolute; position: relative;
right: 0; right: 0;
border-radius: 0; border-radius: 0;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
@ -92,14 +150,15 @@
.movies.mass_edit_list .movie .data { .movies.mass_edit_list .movie .data {
height: 30px; height: 30px;
padding: 3px 0 3px 10px; padding: 3px 0 3px 10px;
width: 938px;
box-shadow: none; box-shadow: none;
border: 0; border: 0;
background: none; background: #4e5969;
} }
.movies.thumbs_list .data { .movies.thumbs_list .data {
position: absolute;
left: 0; left: 0;
top: 0;
width: 100%; width: 100%;
padding: 10px; padding: 10px;
height: 100%; height: 100%;
@ -144,7 +203,10 @@
height: 100%; height: 100%;
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
}
.movies.thumbs_list .poster {
position: relative;
border-radius: 0;
} }
.movies.list_list .movie:not(.details_view) .poster, .movies.list_list .movie:not(.details_view) .poster,
.movies.mass_edit_list .poster { .movies.mass_edit_list .poster {
@ -159,38 +221,72 @@
.movies.thumbs_list .poster { .movies.thumbs_list .poster {
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: none;
} }
.movies .poster img, .movies .poster img,
.options .poster img { .options .poster img {
width: 101%; width: 100%;
height: 101%; height: 100%;
}
.movies.thumbs_list .poster img {
height: auto;
width: 100%;
top: 0;
bottom: 0;
border-radius: 0;
} }
.movies .info { .movies .info {
position: relative; position: relative;
height: 100%; height: 100%;
width: 100%;
} }
.movies .info .title { .movies .info .title {
display: inline;
position: absolute;
font-size: 28px; font-size: 28px;
font-weight: bold; font-weight: bold;
margin-bottom: 10px; margin-bottom: 10px;
margin-top: 2px;
left: 0; left: 0;
top: 0; top: 0;
width: 90%; width: 100%;
padding-right: 80px;
transition: all 0.2s linear; transition: all 0.2s linear;
} }
.movies .info .title span {
display: block;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 100%;
height: 30px;
line-height: 30px;
top: -5px;
position: relative;
}
.movies.thumbs_list .info .title span {
white-space: normal;
overflow: auto;
height: auto;
text-align: left;
}
@media all and (max-width: 480px) {
.movies.thumbs_list .movie .info .title span,
.movies.thumbs_list .movie .info .year {
font-size: 15px;
line-height: 15px;
overflow: hidden;
}
}
.movies.list_list .movie:not(.details_view) .info .title, .movies.list_list .movie:not(.details_view) .info .title,
.movies.mass_edit_list .info .title { .movies.mass_edit_list .info .title {
font-size: 16px; font-size: 16px;
font-weight: normal; font-weight: normal;
text-overflow: ellipsis;
width: auto; width: auto;
overflow: hidden;
} }
.movies.thumbs_list .movie:not(.no_thumbnail) .info { .movies.thumbs_list .movie:not(.no_thumbnail) .info {
@ -202,25 +298,22 @@
.movies.thumbs_list .info .title { .movies.thumbs_list .info .title {
font-size: 21px; font-size: 21px;
text-shadow: 0 0 10px #000;
word-wrap: break-word; word-wrap: break-word;
padding: 0;
} }
.movies .info .year { .movies .info .year {
position: absolute; position: absolute;
font-size: 30px;
margin-bottom: 10px;
color: #bbb; color: #bbb;
width: 10%;
right: 0; right: 0;
top: 0; top: 1px;
text-align: right; text-align: right;
transition: all 0.2s linear; transition: all 0.2s linear;
font-weight: normal;
} }
.movies.list_list .movie:not(.details_view) .info .year, .movies.list_list .movie:not(.details_view) .info .year,
.movies.mass_edit_list .info .year { .movies.mass_edit_list .info .year {
font-size: 16px; font-size: 1.25em;
width: 6%;
right: 10px; right: 10px;
} }
@ -232,16 +325,14 @@
top: auto; top: auto;
right: auto; right: auto;
color: #FFF; color: #FFF;
text-shadow: none;
text-shadow: 0 0 6px #000;
} }
.movies .info .description { .movies .info .description {
position: absolute;
top: 30px; top: 30px;
clear: both; clear: both;
height: 80px; bottom: 30px;
overflow: hidden; overflow: hidden;
position: absolute;
} }
.movies .data:hover .description { .movies .data:hover .description {
overflow: auto; overflow: auto;
@ -254,10 +345,15 @@
.movies .data .quality { .movies .data .quality {
position: absolute; position: absolute;
bottom: 0; bottom: 2px;
display: block; display: block;
min-height: 20px; min-height: 20px;
vertical-align: mid; }
@media all and (max-width: 480px) {
.movies .data .quality {
display: none;
}
} }
.movies .status_suggest .data .quality, .movies .status_suggest .data .quality,
@ -275,9 +371,8 @@
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
text-transform: uppercase; text-transform: uppercase;
text-shadow: none;
font-weight: normal; font-weight: normal;
margin: 0 2px; margin: 0 4px 0 0;
border-radius: 2px; border-radius: 2px;
background-color: rgba(255,255,255,0.1); background-color: rgba(255,255,255,0.1);
} }
@ -285,7 +380,7 @@
.movies.mass_edit_list .data .quality { .movies.mass_edit_list .data .quality {
text-align: right; text-align: right;
right: 0; right: 0;
margin-right: 50px; margin-right: 60px;
z-index: 1; z-index: 1;
} }
@ -315,18 +410,40 @@
bottom: 20px; bottom: 20px;
right: 20px; right: 20px;
line-height: 0; line-height: 0;
margin-top: -25px; top: 0;
opacity: 0;
display: none;
width: 0;
}
@media all and (max-width: 480px) {
.movies .data .actions {
display: none !important;
}
}
.movies .movie:hover .data .actions {
opacity: 1;
display: inline-block;
width: auto;
}
.movies.details_list .data .actions {
top: auto;
bottom: 18px;
}
.movies .movie:hover .actions {
opacity: 1;
display: inline-block;
} }
.movies.thumbs_list .data .actions { .movies.thumbs_list .data .actions {
bottom: 8px; bottom: 2px;
right: 10px; right: 10px;
top: auto;
} }
.movies .data:hover .action { opacity: 0.6; } .movies .movie:hover .action { opacity: 0.6; }
.movies .data:hover .action:hover { opacity: 1; } .movies .movie:hover .action:hover { opacity: 1; }
.movies.mass_edit_list .data .actions {
display: none;
}
.movies .data .action { .movies .data .action {
background-repeat: no-repeat; background-repeat: no-repeat;
@ -335,11 +452,14 @@
width: 26px; width: 26px;
height: 26px; height: 26px;
padding: 3px; padding: 3px;
opacity: 0;
} }
.movies.list_list .movie:not(.details_view) .data:hover .actions, .movies.mass_edit_list .movie .data .actions {
.movies.mass_edit_list .data:hover .actions { display: none;
}
.movies.list_list .movie:not(.details_view):hover .actions,
.movies.mass_edit_list .movie:hover .actions {
margin: 0; margin: 0;
background: #4e5969; background: #4e5969;
top: 2px; top: 2px;
@ -354,7 +474,8 @@
font-size: 20px; font-size: 20px;
position: absolute; position: absolute;
padding: 70px 0 0; padding: 70px 0 0;
width: 100%; left: 120px;
right: 0;
} }
.movies .delete_container .cancel { .movies .delete_container .cancel {
} }
@ -372,8 +493,8 @@
.movies .options { .movies .options {
position: absolute; position: absolute;
margin-left: 120px; right: 0;
width: 840px; left: 120px;
} }
.movies .options .form { .movies .options .form {
@ -396,7 +517,6 @@
.movies .options .table .item.ignored span { .movies .options .table .item.ignored span {
text-decoration: line-through; text-decoration: line-through;
color: rgba(255,255,255,0.4); color: rgba(255,255,255,0.4);
text-shadow: none;
} }
.movies .options .table .item.ignored .delete { .movies .options .table .item.ignored .delete {
background-image: url('../images/icon.undo.png'); background-image: url('../images/icon.undo.png');
@ -430,7 +550,7 @@
overflow: hidden; overflow: hidden;
} }
.movies .options .table .name { .movies .options .table .name {
width: 350px; width: 340px;
overflow: hidden; overflow: hidden;
text-align: left; text-align: left;
padding: 0 10px; padding: 0 10px;
@ -464,6 +584,9 @@
text-align: center; text-align: center;
transition: all .6s cubic-bezier(0.9,0,0.1,1); transition: all .6s cubic-bezier(0.9,0,0.1,1);
overflow: hidden; overflow: hidden;
left: 0;
position: absolute;
z-index: 10;
} }
.movies .movie .trailer_container.hide { .movies .movie .trailer_container.hide {
height: 0 !important; height: 0 !important;
@ -480,6 +603,7 @@
background: #4e5969; background: #4e5969;
border-radius: 0 0 2px 2px; border-radius: 0 0 2px 2px;
transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s; transition: all .2s cubic-bezier(0.9,0,0.1,1) .2s;
z-index: 11;
} }
.movies .movie .hide_trailer.hide { .movies .movie .hide_trailer.hide {
top: -30px; top: -30px;
@ -510,6 +634,54 @@
.movies .movie .releases .last_release > :first-child { .movies .movie .releases .last_release > :first-child {
margin-left: -6px; margin-left: -6px;
} }
.movies .movie .trynext {
display: inline;
position: absolute;
right: 135px;
z-index: 2;
opacity: 0;
background: #4e5969;
min-width: 300px;
text-align: right;
height: 100%;
padding: 3px 0;
top: 0;
}
@media all and (max-width: 480px) {
.movies .movie .trynext {
display: none;
}
}
.movies.mass_edit_list .trynext { display: none; }
.wanted .movies .movie .trynext {
padding-right: 50px;
}
.movies .movie:hover .trynext {
opacity: 1;
}
.movies.details_list .movie .trynext {
background: #47515f;
padding: 0;
right: 0;
bottom: 35px;
height: auto;
}
.movies .movie .trynext a {
background-position: 5px center;
padding: 0 5px 0 25px;
margin-right: 10px;
color: #FFF;
border-radius: 2px;
}
.movies .movie .trynext a:last-child {
margin: 0;
}
.movies .movie .trynext a:hover {
background-color: #369545;
}
.movies .load_more { .movies .load_more {
display: block; display: block;
@ -523,24 +695,32 @@
.movies .alph_nav { .movies .alph_nav {
transition: box-shadow .4s linear; transition: box-shadow .4s linear;
position: fixed; position: relative;
z-index: 4; z-index: 4;
top: 0; top: 0px;
padding: 100px 60px 7px; right: 0;
width: 1080px; margin: 0 auto;
margin: 0 -60px; width: 100%;
box-shadow: 0 20px 20px -22px rgba(0,0,0,0.1); padding: 10px 0;
background: #4e5969;
} }
.movies .alph_nav.float { @media all and (max-width: 480px) {
box-shadow: 0 30px 30px -32px rgba(0,0,0,0.5); .movies .alph_nav {
border-radius: 0; display: none;
}
} }
.movies .alph_nav ul.numbers, .movies .alph_nav > div {
position: relative;
max-width: 980px;
margin: 0 auto;
padding: 0;
min-height: 24px;
}
.movies .alph_nav .numbers,
.movies .alph_nav .counter, .movies .alph_nav .counter,
.movies .alph_nav ul.actions { .movies .alph_nav .actions {
list-style: none; list-style: none;
padding: 0 0 1px; padding: 0 0 1px;
margin: 0; margin: 0;
@ -549,49 +729,62 @@
} }
.movies .alph_nav .counter { .movies .alph_nav .counter {
width: 60px; text-align: right;
text-align: center; position: absolute;
right: 270px;
background: #4e5969;
padding: 4px 10px;
} }
.movies .alph_nav .numbers li, .movies .alph_nav .numbers li,
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
width: 20px;
height: 24px; height: 24px;
line-height: 26px; line-height: 23px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
color: rgba(255,255,255,0.2); color: rgba(255,255,255,0.2);
border: 1px solid transparent; border: 1px solid transparent;
transition: all 0.1s ease-in-out; transition: all 0.1s ease-in-out;
text-shadow: none;
} }
.movies .alph_nav .numbers li:first-child {
width: 43px; @media all and (max-width: 900px) {
.movies .alph_nav .numbers {
display: none;
}
} }
.movies .alph_nav .numbers li {
width: auto;
padding: 0 4px;
}
.movies .alph_nav li.available { .movies .alph_nav li.available {
color: rgba(255,255,255,0.8); color: #FFF;
font-weight: bolder; font-weight: bolder;
} }
.movies .alph_nav li.active.available, .movies .alph_nav li.available:hover { .movies .alph_nav li.active.available,
color: #fff; .movies .alph_nav li.available:hover {
font-size: 20px; background: rgba(255,255,255,.1);
line-height: 20px;
} }
.movies .alph_nav input { .movies .alph_nav .search {
padding: 6px 5px; padding: 6px 5px;
margin: 0 0 0 6px; margin: 0 0 0 20px;
float: left; position: absolute;
width: 155px; right: 30px;
width: 154px;
height: 25px; height: 25px;
transition: all 0.6s cubic-bezier(0.9,0,0.1,1);
} }
.movies .alph_nav .actions { .movies .alph_nav .actions {
margin: 0 6px 0 0; margin: 0 6px 0 0;
-moz-user-select: none; -moz-user-select: none;
position: absolute;
right: 183px;
} }
.movies .alph_nav .actions li { .movies .alph_nav .actions li {
border-radius: 1px; border-radius: 1px;
@ -675,10 +868,12 @@
} }
.movies .alph_nav .more_menu { .movies .alph_nav .more_menu {
margin-left: 48px; right: 0;
position: absolute;
} }
.movies .alph_nav .more_menu > a { .movies .alph_nav .more_menu > a {
background-color: #4e5969;
background-position: center -158px; background-position: center -158px;
} }
@ -735,7 +930,6 @@
font-weight: bold; font-weight: bold;
display: inline-block; display: inline-block;
text-transform: uppercase; text-transform: uppercase;
text-shadow: none;
font-weight: normal; font-weight: normal;
font-size: 20px; font-size: 20px;
border-left: 1px solid rgba(255, 255, 255, .2); border-left: 1px solid rgba(255, 255, 255, .2);

13
couchpotato/core/plugins/movie/static/movie.js

@ -22,7 +22,10 @@ var Movie = new Class({
addEvents: function(){ addEvents: function(){
var self = this; var self = this;
App.addEvent('movie.update.'+self.data.id, self.update.bind(self)); App.addEvent('movie.update.'+self.data.id, function(notification){
self.busy(false)
self.update.delay(2000, self, notification);
});
['movie.busy', 'searcher.started'].each(function(listener){ ['movie.busy', 'searcher.started'].each(function(listener){
App.addEvent(listener+'.'+self.data.id, function(notification){ App.addEvent(listener+'.'+self.data.id, function(notification){
@ -57,6 +60,7 @@ var Movie = new Class({
var self = this; var self = this;
if(!set_busy){ if(!set_busy){
setTimeout(function(){
if(self.spinner){ if(self.spinner){
self.mask.fade('out'); self.mask.fade('out');
setTimeout(function(){ setTimeout(function(){
@ -68,6 +72,7 @@ var Movie = new Class({
self.mask = null; self.mask = null;
}, 400); }, 400);
} }
}, 1000)
} }
else if(!self.spinner) { else if(!self.spinner) {
self.createMask(); self.createMask();
@ -126,12 +131,14 @@ var Movie = new Class({
self.thumbnail = File.Select.single('poster', self.data.library.files), self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light').adopt( self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt( self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', { new Element('div.title').adopt(
self.title = new Element('span', {
'text': self.getTitle() || 'n/a' 'text': self.getTitle() || 'n/a'
}), }),
self.year = new Element('div.year', { self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a' 'text': self.data.library.year || 'n/a'
}), })
),
self.rating = new Element('div.rating.icon', { self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating 'text': self.data.library.rating
}), }),

224
couchpotato/core/plugins/movie/static/search.css

@ -1,113 +1,153 @@
.search_form { .search_form {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: middle;
width: 25%; position: absolute;
right: 105px;
top: 0;
text-align: right;
height: 100%;
border-bottom: 4px solid transparent;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
position: absolute;
z-index: 20;
border: 1px solid transparent;
border-width: 0 0 4px;
} }
.search_form:hover {
border-color: #047792;
}
.search_form input { @media all and (max-width: 480px) {
padding: 4px 20px 4px 4px; .search_form {
margin: 0; right: 44px;
font-size: 14px;
width: 100%;
height: 24px;
} }
.search_form input:focus {
padding-right: 83px;
} }
.search_form .input .enter { .search_form.focused,
background: #369545 url('../images/sprite.png') right -188px no-repeat; .search_form.shown {
padding: 0 20px 0 4px; border-color: #04bce6;
border-radius: 2px;
text-transform: uppercase;
font-size: 10px;
margin-left: -78px;
display: inline-block;
opacity: 0;
position: relative;
top: -2px;
cursor: pointer;
vertical-align: middle;
visibility: hidden;
} }
.search_form.focused .input .enter {
visibility: visible; .search_form .input {
height: 100%;
overflow: hidden;
width: 45px;
transition: all .4s cubic-bezier(0.9,0,0.1,1);
} }
.search_form.focused.filled .input .enter {
.search_form.focused .input,
.search_form.shown .input {
width: 380px;
background: #4e5969;
}
.search_form .input input {
border-radius: 0;
display: block;
width: 100%;
border: 0;
background: rgba(255,255,255,.08);
color: #FFF;
font-size: 25px;
height: 100%;
padding: 10px;
width: 100%;
opacity: 0;
padding: 0 40px 0 10px;
transition: all .4s ease-in-out .2s;
}
.search_form.focused .input input,
.search_form.shown .input input {
opacity: 1; opacity: 1;
} }
@media all and (max-width: 480px) {
.search_form .input input {
font-size: 15px;
}
.search_form.focused .input,
.search_form.shown .input {
width: 277px;
}
}
.search_form .input a { .search_form .input a {
width: 17px; position: absolute;
height: 20px; top: 0;
display: inline-block; right: 0;
margin: -2px 0 0 2px; width: 44px;
top: 4px; height: 100%;
right: 5px;
background: url('../images/sprite.png') left -37px no-repeat;
cursor: pointer; cursor: pointer;
opacity: 0;
transition: all 0.2s ease-in-out;
vertical-align: middle; vertical-align: middle;
text-align: center;
line-height: 66px;
font-size: 15px;
color: #FFF;
} }
.search_form.filled .input a { .search_form .input a:after {
opacity: 1; content: "\e03e";
}
.search_form.shown.filled .input a:after {
content: "\e04e";
}
@media all and (max-width: 480px) {
.search_form .input a {
line-height: 44px;
}
} }
.search_form .results_container { .search_form .results_container {
text-align: left;
position: absolute; position: absolute;
background: #5c697b; background: #5c697b;
margin: 6px 0 0 -230px; margin: 4px 0 0;
width: 470px; width: 470px;
min-height: 140px; min-height: 140px;
border-radius: 3px;
box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55); box-shadow: 0 20px 20px -10px rgba(0,0,0,0.55);
display: none; display: none;
} }
.search_form.shown.filled .results_container { @media all and (max-width: 480px) {
display: block; .search_form .results_container {
width: 320px;
} }
}
.search_form .results_container:before { .search_form.focused.filled .results_container,
content: ' '; .search_form.shown.filled .results_container {
height: 0;
position: relative;
width: 0;
border: 10px solid transparent;
border-bottom-color: #5c697b;
display: block; display: block;
top: -20px;
left: 346px;
} }
.search_form .results { .search_form .results {
max-height: 570px; max-height: 570px;
overflow-x: hidden; overflow-x: hidden;
padding: 10px 0;
margin-top: -18px;
} }
.movie_result { .movie_result {
overflow: hidden; overflow: hidden;
height: 140px; height: 50px;
position: relative; position: relative;
} }
.movie_result .options { .movie_result .options {
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%;
top: 0; top: 0;
left: 0; left: 30px;
right: 0;
padding: 13px;
border: 1px solid transparent; border: 1px solid transparent;
border-width: 1px 0; border-width: 1px 0;
border-radius: 0; border-radius: 0;
box-shadow: inset 0 1px 8px rgba(0,0,0,0.25); box-shadow: inset 0 1px 8px rgba(0,0,0,0.25);
} }
.movie_result .options > .in_library_wanted {
margin-top: -7px;
}
.movie_result .options > div { .movie_result .options > div {
padding: 0 15px;
border: 0; border: 0;
} }
@ -123,6 +163,13 @@
.movie_result .options select[name=title] { width: 180px; } .movie_result .options select[name=title] { width: 180px; }
.movie_result .options select[name=profile] { width: 90px; } .movie_result .options select[name=profile] { width: 90px; }
@media all and (max-width: 480px) {
.movie_result .options select[name=title] { width: 90px; }
.movie_result .options select[name=profile] { width: 60px; }
}
.movie_result .options .button { .movie_result .options .button {
vertical-align: middle; vertical-align: middle;
display: inline-block; display: inline-block;
@ -130,25 +177,21 @@
.movie_result .options .message { .movie_result .options .message {
height: 100%; height: 100%;
line-height: 140px;
font-size: 20px; font-size: 20px;
text-align: center;
color: #fff; color: #fff;
line-height: 20px;
} }
.movie_result .data { .movie_result .data {
padding: 0 15px;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%;
top: 0; top: 0;
left: 0; left: 30px;
right: 0;
background: #5c697b; background: #5c697b;
cursor: pointer; cursor: pointer;
border-top: 1px solid rgba(255,255,255, 0.08);
border-bottom: 1px solid #333; transition: all .4s cubic-bezier(0.9,0,0.1,1);
border-top: 1px solid rgba(255,255,255, 0.15);
transition: all .6s cubic-bezier(0.9,0,0.1,1);
} }
.movie_result .data.open { .movie_result .data.open {
left: 100%; left: 100%;
@ -158,53 +201,46 @@
.movie_result .in_wanted, .movie_result .in_library { .movie_result .in_wanted, .movie_result .in_library {
position: absolute; position: absolute;
margin-top: 105px; bottom: 2px;
left: 14px;
font-size: 11px;
} }
.movie_result .thumbnail { .movie_result .thumbnail {
width: 17%; width: 34px;
display: inline-block; min-height: 100%;
margin: 15px 3% 15px 0; display: block;
margin: 0;
vertical-align: top; vertical-align: top;
border-radius: 3px;
box-shadow: 0 0 3px rgba(0,0,0,0.35);
} }
.movie_result .info { .movie_result .info {
width: 80%; position: absolute;
display: inline-block; top: 20%;
vertical-align: top; left: 15px;
padding: 15px 0; right: 60px;
height: 120px; vertical-align: middle;
overflow: hidden;
}
.movie_result .info .tagline {
max-height: 70px;
overflow: hidden;
display: inline-block;
}
.movie_result .add +.info {
margin-left: 20%;
} }
.movie_result .info h2 { .movie_result .info h2 {
font-weight: normal;
font-size: 20px;
display: block;
margin: 0; margin: 0;
font-size: 17px; text-overflow: ellipsis;
line-height: 20px; overflow: hidden;
white-space: nowrap;
width: 100%;
} }
.movie_result .info h2 span { .movie_result .info h2 span {
padding: 0 5px; padding: 0 5px;
position: absolute;
right: -60px;
} }
.movie_result .info h2 span:before { content: "("; }
.movie_result .info h2 span:after { content: ")"; }
.search_form .mask, .search_form .mask,
.movie_result .mask { .movie_result .mask {
border-radius: 3px;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 100%; width: 100%;

71
couchpotato/core/plugins/movie/static/search.js

@ -7,33 +7,30 @@ Block.Search = new Class({
create: function(){ create: function(){
var self = this; var self = this;
var focus_timer = 0;
self.el = new Element('div.search_form').adopt( self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt( new Element('div.input').adopt(
self.input = new Element('input.inlay', { self.input = new Element('input', {
'placeholder': 'Search & add a new movie', 'placeholder': 'Search & add a new movie',
'events': { 'events': {
'keyup': self.keyup.bind(self), 'keyup': self.keyup.bind(self),
'focus': function(){ 'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused') self.el.addClass('focused')
if(this.get('value')) if(this.get('value'))
self.hideResults(false) self.hideResults(false)
}, },
'blur': function(){ 'blur': function(){
(function(){ focus_timer = (function(){
self.el.removeClass('focused') self.el.removeClass('focused')
}).delay(2000); }).delay(100);
} }
} }
}), }),
new Element('span.enter', { new Element('a.icon2', {
'events': { 'events': {
'click': self.keyup.bind(self) 'click': self.clear.bind(self),
}, 'touchend': self.clear.bind(self)
'text':'Enter'
}),
new Element('a', {
'events': {
'click': self.clear.bind(self)
} }
}) })
), ),
@ -59,6 +56,12 @@ Block.Search = new Class({
var self = this; var self = this;
(e).preventDefault(); (e).preventDefault();
if(self.last_q === ''){
self.input.blur()
self.last_q = null;
}
else {
self.last_q = ''; self.last_q = '';
self.input.set('value', ''); self.input.set('value', '');
self.input.focus() self.input.focus()
@ -66,6 +69,8 @@ Block.Search = new Class({
self.movies = [] self.movies = []
self.results.empty() self.results.empty()
self.el.removeClass('filled') self.el.removeClass('filled')
}
}, },
hideResults: function(bool){ hideResults: function(bool){
@ -92,8 +97,10 @@ Block.Search = new Class({
self.el[self.q() ? 'addClass' : 'removeClass']('filled') self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click')) if(self.q() != self.last_q){
self.autocomplete() if(self.autocomplete_timer) clearTimeout(self.autocomplete_timer)
self.autocomplete_timer = self.autocomplete.delay(300, self)
}
}, },
@ -197,6 +204,11 @@ Block.Search.Item = new Class({
self.el = new Element('div.movie_result', { self.el = new Element('div.movie_result', {
'id': info.imdb 'id': info.imdb
}).adopt( }).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
self.options_el = new Element('div.options.inlay'), self.options_el = new Element('div.options.inlay'),
self.data_container = new Element('div.data', { self.data_container = new Element('div.data', {
'tween': { 'tween': {
@ -207,11 +219,6 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self) 'click': self.showOptions.bind(self)
} }
}).adopt( }).adopt(
self.thumbnail = info.images && info.images.poster.length > 0 ? new Element('img.thumbnail', {
'src': info.images.poster[0],
'height': null,
'width': null
}) : null,
new Element('div.info').adopt( new Element('div.info').adopt(
self.title = new Element('h2', { self.title = new Element('h2', {
'text': info.titles[0] 'text': info.titles[0]
@ -219,28 +226,11 @@ Block.Search.Item = new Class({
self.year = info.year ? new Element('span.year', { self.year = info.year ? new Element('span.year', {
'text': info.year 'text': info.year
}) : null }) : null
), )
self.tagline = new Element('span.tagline', {
'text': info.tagline ? info.tagline : info.plot,
'title': info.tagline ? info.tagline : info.plot
}),
self.director = self.info.director ? new Element('span.director', {
'text': 'Director:' + info.director
}) : null,
self.starring = info.actors ? new Element('span.actors', {
'text': 'Starring:'
}) : null
) )
) )
) )
if(info.actors){
Object.each(info.actors, function(actor){
new Element('span', {
'text': actor
}).inject(self.starring)
})
}
info.titles.each(function(title){ info.titles.each(function(title){
self.alternativeTitle({ self.alternativeTitle({
@ -319,12 +309,9 @@ Block.Search.Item = new Class({
} }
self.options_el.grab( self.options_el.grab(
new Element('div').adopt( new Element('div', {
self.thumbnail = (info.images && info.images.poster.length > 0) ? new Element('img.thumbnail', { 'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : ''
'src': info.images.poster[0], }).adopt(
'height': null,
'width': null
}) : null,
self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', { self.info.in_wanted && self.info.in_wanted.profile ? new Element('span.in_wanted', {
'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label 'text': 'Already in wanted list: ' + self.info.in_wanted.profile.label
}) : (in_library ? new Element('span.in_library', { }) : (in_library ? new Element('span.in_library', {

20
couchpotato/core/plugins/profile/main.py

@ -5,7 +5,7 @@ from couchpotato.core.helpers.encoding import toUnicode
from couchpotato.core.helpers.request import jsonified, getParams, getParam from couchpotato.core.helpers.request import jsonified, getParams, getParam
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Profile, ProfileType from couchpotato.core.settings.model import Profile, ProfileType, Movie
log = CPLog(__name__) log = CPLog(__name__)
@ -30,6 +30,21 @@ class ProfilePlugin(Plugin):
}) })
addEvent('app.initialize', self.fill, priority = 90) addEvent('app.initialize', self.fill, priority = 90)
addEvent('app.load', self.forceDefaults)
def forceDefaults(self):
# Get all active movies without profile
active_status = fireEvent('status.get', 'active', single = True)
db = get_session()
movies = db.query(Movie).filter(Movie.status_id == active_status.get('id'), Movie.profile == None).all()
if len(movies) > 0:
default_profile = self.default()
for movie in movies:
movie.profile_id = default_profile.get('id')
db.commit()
def allView(self): def allView(self):
@ -129,6 +144,9 @@ class ProfilePlugin(Plugin):
db.delete(p) db.delete(p)
db.commit() db.commit()
# Force defaults on all empty profile movies
self.forceDefaults()
success = True success = True
except Exception, e: except Exception, e:
message = log.error('Failed deleting Profile: %s', e) message = log.error('Failed deleting Profile: %s', e)

39
couchpotato/core/plugins/profile/static/profile.css

@ -6,15 +6,25 @@
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
} }
.profile { border-bottom: 1px solid rgba(255,255,255,0.2) } .profile {
border-bottom: 1px solid rgba(255,255,255,0.2);
position: relative;
}
.profile > .delete { .profile > .delete {
height: 20px;
width: 20px;
position: absolute; position: absolute;
margin-left: 690px; padding: 25px 20px;
padding: 14px;
background-position: center; background-position: center;
right: 0;
cursor: pointer;
opacity: 0.6;
}
.profile > .delete:hover {
opacity: 1;
}
.profile .ctrlHolder:hover {
background: none;
} }
.profile .qualities { .profile .qualities {
@ -34,7 +44,8 @@
.profile .wait_for { .profile .wait_for {
position: absolute; position: absolute;
margin: -45px 0 0 437px; right: 60px;
top: 0;
} }
.profile .wait_for input { .profile .wait_for input {
@ -61,6 +72,10 @@
margin-right: 10px; margin-right: 10px;
} }
.profile .type .check {
margin-top: -1px;
}
.profile .quality_type select { .profile .quality_type select {
width: 186px; width: 186px;
margin-left: -1px; margin-left: -1px;
@ -71,13 +86,13 @@
} }
.profile .types .type .handle { .profile .types .type .handle {
background: url('./handle.png') center; background: url('../../static/profile_plugin/handle.png') center;
display: inline-block; display: inline-block;
height: 20px; height: 20px;
width: 20px; width: 20px;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab; cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
margin: 0; margin: 0;
} }
@ -105,9 +120,9 @@
} }
#profile_ordering li { #profile_ordering li {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab; cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2); border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px; padding: 0 5px;
} }
@ -126,7 +141,7 @@
} }
#profile_ordering li .handle { #profile_ordering li .handle {
background: url('./handle.png') center; background: url('../../static/profile_plugin/handle.png') center;
width: 20px; width: 20px;
float: right; float: right;
} }

6
couchpotato/core/plugins/quality/main.py

@ -19,12 +19,12 @@ class QualityPlugin(Plugin):
qualities = [ qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']}, {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080p', 'width': 1920, 'height': 1080, 'alternative': [], 'allow': [], 'ext':['mkv', 'm2ts'], 'tags': ['m2ts']},
{'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720P', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']}, {'identifier': '720p', 'hd': True, 'size': (3500, 10000), 'label': '720p', 'width': 1280, 'height': 720, 'alternative': [], 'allow': [], 'ext':['mkv', 'ts']},
{'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']}, {'identifier': 'brrip', 'hd': True, 'size': (700, 7000), 'label': 'BR-Rip', 'alternative': ['bdrip'], 'allow': ['720p', '1080p'], 'ext':['avi']},
{'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']}, {'identifier': 'dvdr', 'size': (3000, 10000), 'label': 'DVD-R', 'alternative': [], 'allow': [], 'ext':['iso', 'img'], 'tags': ['pal', 'ntsc', 'video_ts', 'audio_ts']},
{'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]}, {'identifier': 'dvdrip', 'size': (600, 2400), 'label': 'DVD-Rip', 'width': 720, 'alternative': ['dvdrip'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg'], 'tags': [('dvd', 'rip'), ('dvd', 'xvid'), ('dvd', 'divx')]},
{'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'scr', 'size': (600, 1600), 'label': 'Screener', 'alternative': ['screener', 'dvdscr', 'ppvrip', 'dvdscreener'], 'allow': ['dvdr', 'dvd'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'r5', 'size': (600, 1000), 'label': 'R5', 'alternative': [], 'allow': ['dvdr'], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'tc', 'size': (600, 1000), 'label': 'TeleCine', 'alternative': ['telecine'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},
{'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']}, {'identifier': 'ts', 'size': (600, 1000), 'label': 'TeleSync', 'alternative': ['telesync', 'hdts'], 'allow': [], 'ext':['avi', 'mpg', 'mpeg']},

30
couchpotato/core/plugins/release/main.py

@ -41,12 +41,15 @@ class Release(Plugin):
addEvent('release.clean', self.clean) addEvent('release.clean', self.clean)
def add(self, group): def add(self, group):
db = get_session() db = get_session()
identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier']) identifier = '%s.%s.%s' % (group['library']['identifier'], group['meta_data'].get('audio', 'unknown'), group['meta_data']['quality']['identifier'])
done_status, snatched_status = fireEvent('status.get', ['done', 'snatched'], single = True)
# Add movie # Add movie
done_status = fireEvent('status.get', 'done', single = True)
movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first() movie = db.query(Movie).filter_by(library_id = group['library'].get('id')).first()
if not movie: if not movie:
movie = Movie( movie = Movie(
@ -58,7 +61,6 @@ class Release(Plugin):
db.commit() db.commit()
# Add Release # Add Release
snatched_status = fireEvent('status.get', 'snatched', single = True)
rel = db.query(Relea).filter( rel = db.query(Relea).filter(
or_( or_(
Relea.identifier == identifier, Relea.identifier == identifier,
@ -76,12 +78,16 @@ class Release(Plugin):
db.commit() db.commit()
# Add each file type # Add each file type
added_files = []
for type in group['files']: for type in group['files']:
for cur_file in group['files'][type]: for cur_file in group['files'][type]:
added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie') added_file = self.saveFile(cur_file, type = type, include_media_info = type is 'movie')
added_files.append(added_file.get('id'))
# Add the release files in batch
try: try:
added_file = db.query(File).filter_by(id = added_file.get('id')).one() added_files = db.query(File).filter(or_(*[File.id == x for x in added_files])).all()
rel.files.append(added_file) rel.files.extend(added_files)
db.commit() db.commit()
except Exception, e: except Exception, e:
log.debug('Failed to attach "%s" to release: %s', (cur_file, e)) log.debug('Failed to attach "%s" to release: %s', (cur_file, e))
@ -147,8 +153,7 @@ class Release(Plugin):
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
ignored_status = fireEvent('status.get', 'ignored', single = True) ignored_status, available_status = fireEvent('status.get', ['ignored', 'available'], single = True)
available_status = fireEvent('status.get', 'available', single = True)
rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id') rel.status_id = available_status.get('id') if rel.status_id is ignored_status.get('id') else ignored_status.get('id')
db.commit() db.commit()
@ -160,7 +165,8 @@ class Release(Plugin):
db = get_session() db = get_session()
id = getParam('id') id = getParam('id')
status_snatched = fireEvent('status.add', 'snatched', single = True)
snatched_status, done_status = fireEvent('status.get', ['snatched', 'done'], single = True)
rel = db.query(Relea).filter_by(id = id).first() rel = db.query(Relea).filter_by(id = id).first()
if rel: if rel:
@ -168,6 +174,8 @@ class Release(Plugin):
for info in rel.info: for info in rel.info:
item[info.identifier] = info.value item[info.identifier] = info.value
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Snatching "%s"' % item['name'])
# Get matching provider # Get matching provider
provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True) provider = fireEvent('provider.belongs_to', item['url'], provider = item.get('provider'), single = True)
@ -182,9 +190,15 @@ class Release(Plugin):
}), manual = True, single = True) }), manual = True, single = True)
if success: if success:
rel.status_id = status_snatched.get('id') db.expunge_all()
rel = db.query(Relea).filter_by(id = id).first() # Get release again
if rel.status_id != done_status.get('id'):
rel.status_id = snatched_status.get('id')
db.commit() db.commit()
fireEvent('notify.frontend', type = 'release.download', data = True, message = 'Successfully snatched "%s"' % item['name'])
return jsonified({ return jsonified({
'success': success 'success': success
}) })

11
couchpotato/core/plugins/renamer/__init__.py

@ -13,7 +13,7 @@ rename_options = {
'thename': 'The Moviename', 'thename': 'The Moviename',
'year': 'Year (2011)', 'year': 'Year (2011)',
'first': 'First letter (M)', 'first': 'First letter (M)',
'quality': 'Quality (720P)', 'quality': 'Quality (720p)',
'video': 'Video (x264)', 'video': 'Video (x264)',
'audio': 'Audio (DTS)', 'audio': 'Audio (DTS)',
'group': 'Releasegroup name', 'group': 'Releasegroup name',
@ -113,6 +113,15 @@ config = [{
'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.', 'description': 'Replace all the spaces with a character. Example: ".", "-" (without quotes). Leave empty to use spaces.',
}, },
{ {
'name': 'file_action',
'label': 'Torrent File Action',
'default': 'move',
'type': 'dropdown',
'values': [('Move', 'move'), ('Copy', 'copy'), ('Hard link', 'hardlink'), ('Sym link', 'symlink'), ('Move & Sym link', 'move_symlink')],
'description': 'Define which kind of file operation you want to use for torrents. Before you start using <a href="http://en.wikipedia.org/wiki/Hard_link">hard links</a> or <a href="http://en.wikipedia.org/wiki/Sym_link">sym links</a>, PLEASE read about their possible drawbacks.',
'advanced': True,
},
{
'advanced': True, 'advanced': True,
'name': 'ntfs_permission', 'name': 'ntfs_permission',
'label': 'NTFS Permission', 'label': 'NTFS Permission',

207
couchpotato/core/plugins/renamer/main.py

@ -2,12 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
from couchpotato.core.helpers.encoding import toUnicode, ss from couchpotato.core.helpers.encoding import toUnicode, ss
from couchpotato.core.helpers.request import jsonified from couchpotato.core.helpers.request import getParams, jsonified
from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \ from couchpotato.core.helpers.variable import getExt, mergeDicts, getTitle, \
getImdb getImdb, link, symlink
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin from couchpotato.core.plugins.base import Plugin
from couchpotato.core.settings.model import Library, File, Profile, Release from couchpotato.core.settings.model import Library, File, Profile, Release, \
ReleaseInfo
from couchpotato.environment import Env from couchpotato.environment import Env
import errno import errno
import os import os
@ -18,7 +19,6 @@ import traceback
log = CPLog(__name__) log = CPLog(__name__)
class Renamer(Plugin): class Renamer(Plugin):
renaming_started = False renaming_started = False
@ -27,7 +27,12 @@ class Renamer(Plugin):
def __init__(self): def __init__(self):
addApiView('renamer.scan', self.scanView, docs = { addApiView('renamer.scan', self.scanView, docs = {
'desc': 'For the renamer to check for new files to rename', 'desc': 'For the renamer to check for new files to rename in a folder',
'params': {
'movie_folder': {'desc': 'Optional: The folder of the movie to scan. Keep empty for default renamer folder.'},
'downloader' : {'desc': 'Optional: The downloader this movie has been downloaded with'},
'download_id': {'desc': 'Optional: The downloader\'s nzb/torrent ID'},
},
}) })
addEvent('renamer.scan', self.scan) addEvent('renamer.scan', self.scan)
@ -35,22 +40,42 @@ class Renamer(Plugin):
addEvent('app.load', self.scan) addEvent('app.load', self.scan)
addEvent('app.load', self.checkSnatched) addEvent('app.load', self.checkSnatched)
addEvent('app.load', self.setCrons)
# Enable / disable interval
addEvent('setting.save.renamer.enabled.after', self.setCrons)
addEvent('setting.save.renamer.run_every.after', self.setCrons)
addEvent('setting.save.renamer.force_every.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.remove', 'renamer.check_snatched')
if self.isEnabled() and self.conf('run_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'), single = True)
if self.conf('run_every') > 0: fireEvent('schedule.remove', 'renamer.check_snatched_forced')
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every')) if self.isEnabled() and self.conf('force_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'), single = True)
if self.conf('force_every') > 0: return True
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
def scanView(self): def scanView(self):
fireEventAsync('renamer.scan') params = getParams()
movie_folder = params.get('movie_folder', None)
downloader = params.get('downloader', None)
download_id = params.get('download_id', None)
fireEventAsync('renamer.scan',
movie_folder = movie_folder,
download_info = {'id': download_id, 'downloader': downloader} if download_id else None
)
return jsonified({ return jsonified({
'success': True 'success': True
}) })
def scan(self): def scan(self, movie_folder = None, download_info = None):
if self.isDisabled(): if self.isDisabled():
return return
@ -60,17 +85,42 @@ class Renamer(Plugin):
return return
# Check to see if the "to" folder is inside the "from" folder. # Check to see if the "to" folder is inside the "from" folder.
if not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')): if movie_folder and not os.path.isdir(movie_folder) or not os.path.isdir(self.conf('from')) or not os.path.isdir(self.conf('to')):
log.debug('"To" and "From" have to exist.') l = log.debug if movie_folder else log.error
l('Both the "To" and "From" have to exist.')
return return
elif self.conf('from') in self.conf('to'): elif self.conf('from') in self.conf('to'):
log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.') log.error('The "to" can\'t be inside of the "from" folder. You\'ll get an infinite loop.')
return return
elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]):
groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True) log.error('The "to" and "from" folders can\'t be inside of or the same as the provided movie folder.')
return
self.renaming_started = True self.renaming_started = True
# make sure the movie folder name is included in the search
folder = None
files = []
if movie_folder:
log.info('Scanning movie folder %s...', movie_folder)
movie_folder = movie_folder.rstrip(os.path.sep)
folder = os.path.dirname(movie_folder)
# Get all files from the specified folder
try:
for root, folders, names in os.walk(movie_folder):
files.extend([os.path.join(root, name) for name in names])
except:
log.error('Failed getting files from %s: %s', (movie_folder, traceback.format_exc()))
db = get_session()
# Extend the download info with info stored in the downloaded release
download_info = self.extendDownloadInfo(download_info)
groups = fireEvent('scanner.scan', folder = folder if folder else self.conf('from'),
files = files, download_info = download_info, return_ignored = False, single = True)
destination = self.conf('to') destination = self.conf('to')
folder_name = self.conf('folder_name') folder_name = self.conf('folder_name')
file_name = self.conf('file_name') file_name = self.conf('file_name')
@ -79,12 +129,8 @@ class Renamer(Plugin):
separator = self.conf('separator') separator = self.conf('separator')
# Statusses # Statusses
done_status = fireEvent('status.get', 'done', single = True) done_status, active_status, downloaded_status, snatched_status = \
active_status = fireEvent('status.get', 'active', single = True) fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True)
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
db = get_session()
for group_identifier in groups: for group_identifier in groups:
@ -304,7 +350,7 @@ class Renamer(Plugin):
else: else:
log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label)) log.info('Better quality release already exists for %s, with quality %s', (movie.library.titles[0].title, release.quality.label))
# Add _EXISTS_ to the parent dir # Add exists tag to the .ignore file
self.tagDir(group, 'exists') self.tagDir(group, 'exists')
# Notify on rename fail # Notify on rename fail
@ -325,7 +371,8 @@ class Renamer(Plugin):
db.commit() db.commit()
# Remove leftover files # Remove leftover files
if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers: if self.conf('cleanup') and not self.conf('move_leftover') and remove_leftovers and \
not (self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info)):
log.debug('Removing leftover files') log.debug('Removing leftover files')
for current_file in group['files']['leftover']: for current_file in group['files']['leftover']:
remove_files.append(current_file) remove_files.append(current_file)
@ -350,7 +397,7 @@ class Renamer(Plugin):
os.remove(src) os.remove(src)
parent_dir = os.path.normpath(os.path.dirname(src)) parent_dir = os.path.normpath(os.path.dirname(src))
if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and destination != parent_dir: if delete_folders.count(parent_dir) == 0 and os.path.isdir(parent_dir) and not parent_dir in [destination, movie_folder] and not self.conf('from') in parent_dir:
delete_folders.append(parent_dir) delete_folders.append(parent_dir)
except: except:
@ -375,12 +422,15 @@ class Renamer(Plugin):
self.makeDir(os.path.dirname(dst)) self.makeDir(os.path.dirname(dst))
try: try:
self.moveFile(src, dst) self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info))
group['renamed_files'].append(dst) group['renamed_files'].append(dst)
except: except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc())) log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
self.tagDir(group, 'failed_rename') self.tagDir(group, 'failed_rename')
if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info):
self.tagDir(group, 'renamed already')
# Remove matching releases # Remove matching releases
for release in remove_releases: for release in remove_releases:
log.debug('Removing release %s', release.identifier) log.debug('Removing release %s', release.identifier)
@ -426,35 +476,39 @@ class Renamer(Plugin):
return rename_files return rename_files
# This adds a file to ignore / tag a release so it is ignored later
def tagDir(self, group, tag): def tagDir(self, group, tag):
rename_files = {} ignore_file = None
for movie_file in sorted(list(group['files']['movie'])):
ignore_file = '%s.ignore' % os.path.splitext(movie_file)[0]
break
if group['dirname']: text = """This file is from CouchPotato
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname'])) It has marked this release as "%s"
else: # Add it to filename This file hides the release from the renamer
for file_type in group['files']: Remove it if you want it to be renamed (again, or at least let it try again)
for rename_me in group['files'][file_type]: """ % tag
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 ignore_file:
if rename_files[src]: self.createFile(ignore_file, text)
dst = rename_files[src]
log.info('Renaming "%s" to "%s"', (src, dst))
# Create dir
self.makeDir(os.path.dirname(dst))
try:
self.moveFile(src, dst)
except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
raise
def moveFile(self, old, dest): def moveFile(self, old, dest, forcemove = False):
dest = ss(dest) dest = ss(dest)
try: try:
if forcemove:
shutil.move(old, dest)
elif self.conf('file_action') == 'hardlink':
link(old, dest)
elif self.conf('file_action') == 'symlink':
symlink(old, dest)
elif self.conf('file_action') == 'copy':
shutil.copy(old, dest)
elif self.conf('file_action') == 'move_symlink':
shutil.move(old, dest)
symlink(dest, old)
else:
shutil.move(old, dest) shutil.move(old, dest)
try: try:
@ -524,16 +578,14 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc())) loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self): def checkSnatched(self):
if self.checking_snatched: if self.checking_snatched:
log.debug('Already checking snatched') log.debug('Already checking snatched')
self.checking_snatched = True self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True) snatched_status, ignored_status, failed_status, done_status = \
ignored_status = fireEvent('status.get', 'ignored', single = True) fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
done_status = fireEvent('status.get', 'done', single = True)
db = get_session() db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all() rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
@ -571,8 +623,16 @@ class Renamer(Plugin):
found = False found = False
for item in statuses: for item in statuses:
found_release = False
if rel_dict['info'].get('download_id'):
if item['id'] == rel_dict['info']['download_id'] and item['downloader'] == rel_dict['info']['download_downloader']:
log.debug('Found release by id: %s', item['id'])
found_release = True
else:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']: if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
found_release = True
if found_release:
timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft'] timeleft = 'N/A' if item['timeleft'] == -1 else item['timeleft']
log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft)) log.debug('Found %s: %s, time to go: %s', (item['name'], item['status'].upper(), timeleft))
@ -580,15 +640,17 @@ class Renamer(Plugin):
pass pass
elif item['status'] == 'failed': elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True) fireEvent('download.remove_failed', item, single = True)
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
else:
rel.status_id = failed_status.get('id') rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time()) rel.last_edit = int(time.time())
db.commit() db.commit()
if self.conf('next_on_failed'):
fireEvent('searcher.try_next_release', movie_id = rel.movie_id)
elif item['status'] == 'completed': elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name']) log.info('Download of %s completed!', item['name'])
if item['id'] and item['downloader'] and item['folder']:
fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item)
else:
scan_required = True scan_required = True
found = True found = True
@ -606,3 +668,38 @@ class Renamer(Plugin):
self.checking_snatched = False self.checking_snatched = False
return True return True
def extendDownloadInfo(self, download_info):
rls = None
if download_info and download_info.get('id') and download_info.get('downloader'):
db = get_session()
rlsnfo_dwnlds = db.query(ReleaseInfo).filter_by(identifier = 'download_downloader', value = download_info.get('downloader')).all()
rlsnfo_ids = db.query(ReleaseInfo).filter_by(identifier = 'download_id', value = download_info.get('id')).all()
for rlsnfo_dwnld in rlsnfo_dwnlds:
for rlsnfo_id in rlsnfo_ids:
if rlsnfo_id.release == rlsnfo_dwnld.release:
rls = rlsnfo_id.release
break
if rls: break
if not rls:
log.error('Download ID %s from downloader %s not found in releases', (download_info.get('id'), download_info.get('downloader')))
if rls:
rls_dict = rls.to_dict({'info':{}})
download_info.update({
'imdb_id': rls.movie.library.identifier,
'quality': rls.quality.identifier,
'type': rls_dict.get('info', {}).get('type')
})
return download_info
def downloadIsTorrent(self, download_info):
return download_info and download_info.get('type') in ['torrent', 'torrent_magnet']

53
couchpotato/core/plugins/scanner/main.py

@ -11,6 +11,7 @@ from subliminal.videos import Video
import enzyme import enzyme
import os import os
import re import re
import threading
import time import time
import traceback import traceback
@ -74,7 +75,7 @@ class Scanner(Plugin):
'hdtv': ['hdtv'] 'hdtv': ['hdtv']
} }
clean = '[ _\,\.\(\)\[\]\-](french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)' clean = '[ _\,\.\(\)\[\]\-](extended.cut|directors.cut|french|swedisch|danish|dutch|swesub|spanish|german|ac3|dts|custom|dc|divx|divx5|dsr|dsrip|dutch|dvd|dvdr|dvdrip|dvdscr|dvdscreener|screener|dvdivx|cam|fragment|fs|hdtv|hdrip|hdtvrip|internal|limited|multisubs|ntsc|ogg|ogm|pal|pdtv|proper|repack|rerip|retail|r3|r5|bd5|se|svcd|swedish|german|read.nfo|nfofix|unrated|ws|telesync|ts|telecine|tc|brrip|bdrip|video_ts|audio_ts|480p|480i|576p|576i|720p|720i|1080p|1080i|hrhd|hrhdtv|hddvd|bluray|x264|h264|xvid|xvidvd|xxx|www.www|cd[1-9]|\[.*\])([ _\,\.\(\)\[\]\-]|$)'
multipart_regex = [ multipart_regex = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1 '[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1 '[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@ -100,7 +101,7 @@ class Scanner(Plugin):
addEvent('scanner.name_year', self.getReleaseNameYear) addEvent('scanner.name_year', self.getReleaseNameYear)
addEvent('scanner.partnumber', self.getPartNumber) addEvent('scanner.partnumber', self.getPartNumber)
def scan(self, folder = None, files = None, simple = False, newer_than = 0, on_found = None): def scan(self, folder = None, files = None, download_info = None, simple = False, newer_than = 0, return_ignored = True, on_found = None):
folder = ss(os.path.normpath(folder)) folder = ss(os.path.normpath(folder))
@ -118,8 +119,7 @@ class Scanner(Plugin):
try: try:
files = [] files = []
for root, dirs, walk_files in os.walk(folder): for root, dirs, walk_files in os.walk(folder):
for filename in walk_files: files.extend(os.path.join(root, filename) for filename in walk_files)
files.append(os.path.join(root, filename))
except: 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: else:
@ -177,17 +177,25 @@ class Scanner(Plugin):
# Group files minus extension # Group files minus extension
ignored_identifiers = []
for identifier, group in movie_files.iteritems(): for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier) if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
log.debug('Grouping files: %s', identifier) log.debug('Grouping files: %s', identifier)
has_ignored = 0
for file_path in group['unsorted_files']: for file_path in group['unsorted_files']:
wo_ext = file_path[:-(len(getExt(file_path)) + 1)] ext = getExt(file_path)
wo_ext = file_path[:-(len(ext) + 1)]
found_files = set([i for i in leftovers if wo_ext in i]) found_files = set([i for i in leftovers if wo_ext in i])
group['unsorted_files'].extend(found_files) group['unsorted_files'].extend(found_files)
leftovers = leftovers - found_files leftovers = leftovers - found_files
has_ignored += 1 if ext == 'ignore' else 0
if has_ignored > 0:
ignored_identifiers.append(identifier)
# Break if CP wants to shut down # Break if CP wants to shut down
if self.shuttingDown(): if self.shuttingDown():
break break
@ -313,6 +321,11 @@ class Scanner(Plugin):
del movie_files del movie_files
# Make sure only one movie was found if a download ID is provided
if download_info and not len(valid_files) == 1:
log.info('Download ID provided (%s), but more than one group found (%s). Ignoring Download ID...', (download_info.get('imdb_id'), len(valid_files)))
download_info = None
# Determine file types # Determine file types
processed_movies = {} processed_movies = {}
total_found = len(valid_files) total_found = len(valid_files)
@ -322,15 +335,17 @@ class Scanner(Plugin):
except: except:
break break
if return_ignored is False and identifier in ignored_identifiers:
log.debug('Ignore file found, ignoring release: %s' % identifier)
continue
# Group extra (and easy) files first # Group extra (and easy) files first
# images = self.getImages(group['unsorted_files'])
group['files'] = { group['files'] = {
'movie_extra': self.getMovieExtras(group['unsorted_files']), 'movie_extra': self.getMovieExtras(group['unsorted_files']),
'subtitle': self.getSubtitles(group['unsorted_files']), 'subtitle': self.getSubtitles(group['unsorted_files']),
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']), 'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']), 'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']), 'trailer': self.getTrailers(group['unsorted_files']),
#'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']), 'leftover': set(group['unsorted_files']),
} }
@ -345,7 +360,7 @@ class Scanner(Plugin):
continue continue
log.debug('Getting metadata for %s', identifier) log.debug('Getting metadata for %s', identifier)
group['meta_data'] = self.getMetaData(group, folder = folder) group['meta_data'] = self.getMetaData(group, folder = folder, download_info = download_info)
# Subtitle meta # Subtitle meta
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {} group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
@ -375,7 +390,7 @@ class Scanner(Plugin):
del group['unsorted_files'] del group['unsorted_files']
# Determine movie # Determine movie
group['library'] = self.determineMovie(group) group['library'] = self.determineMovie(group, download_info = download_info)
if not group['library']: if not group['library']:
log.error('Unable to determine movie: %s', group['identifiers']) log.error('Unable to determine movie: %s', group['identifiers'])
else: else:
@ -388,6 +403,11 @@ class Scanner(Plugin):
if on_found: if on_found:
on_found(group, total_found, total_found - len(processed_movies)) on_found(group, total_found, total_found - len(processed_movies))
# Wait for all the async events calm down a bit
while threading.activeCount() > 100 and not self.shuttingDown():
log.debug('Too many threads active, waiting a few seconds')
time.sleep(10)
if len(processed_movies) > 0: if len(processed_movies) > 0:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder)) log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else: else:
@ -395,7 +415,7 @@ class Scanner(Plugin):
return processed_movies return processed_movies
def getMetaData(self, group, folder = ''): def getMetaData(self, group, folder = '', download_info = None):
data = {} data = {}
files = list(group['files']['movie']) files = list(group['files']['movie'])
@ -417,8 +437,12 @@ class Scanner(Plugin):
if data.get('audio'): break if data.get('audio'): break
# Use the quality guess first, if that failes use the quality we wanted to download
data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True) data['quality'] = fireEvent('quality.guess', files = files, extra = data, single = True)
if not data['quality']: if not data['quality']:
if download_info and download_info.get('quality'):
data['quality'] = fireEvent('quality.single', download_info.get('quality'), single = True)
else:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True) data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD' data['quality_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
@ -495,12 +519,17 @@ class Scanner(Plugin):
return detected_languages return detected_languages
def determineMovie(self, group): def determineMovie(self, group, download_info = None):
imdb_id = None
# Get imdb id from downloader
imdb_id = download_info and download_info.get('imdb_id')
if imdb_id:
log.debug('Found movie via imdb id from it\'s download id: %s', download_info.get('imdb_id'))
files = group['files'] files = group['files']
# Check for CP(imdb_id) string in the file paths # Check for CP(imdb_id) string in the file paths
if not imdb_id:
for cur_file in files['movie']: for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file) imdb_id = self.getCPImdb(cur_file)
if imdb_id: if imdb_id:

10
couchpotato/core/plugins/score/scores.py

@ -116,13 +116,13 @@ def sizeScore(size):
def providerScore(provider): def providerScore(provider):
if provider in ['OMGWTFNZBs', 'PassThePopcorn', 'SceneAccess', 'TorrentLeech']:
return 20
if provider in ['Newznab']: try:
return 10 score = tryInt(Env.setting('extra_score', section = provider.lower(), default = 0))
except:
score = 0
return 0 return score
def duplicateScore(nzb_name, movie_name): def duplicateScore(nzb_name, movie_name):

11
couchpotato/core/plugins/searcher/__init__.py

@ -25,12 +25,13 @@ config = [{
'label': 'Required words', 'label': 'Required words',
'default': '', 'default': '',
'placeholder': 'Example: DTS, AC3 & English', 'placeholder': 'Example: DTS, AC3 & English',
'description': 'Ignore releases that don\'t contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"' 'description': 'A release should contain at least one set of words. Sets are separated by "," and each word within a set must be separated with "&"'
}, },
{ {
'name': 'ignored_words', 'name': 'ignored_words',
'label': 'Ignored words', 'label': 'Ignored words',
'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs', 'default': 'german, dutch, french, truefrench, danish, swedish, spanish, italian, korean, dubbed, swesub, korsub, dksubs',
'description': 'Ignores releases that match any of these sets. (Works like explained above)'
}, },
{ {
'name': 'preferred_method', 'name': 'preferred_method',
@ -40,6 +41,14 @@ config = [{
'type': 'dropdown', 'type': 'dropdown',
'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')], 'values': [('usenet & torrents', 'both'), ('usenet', 'nzb'), ('torrents', 'torrent')],
}, },
{
'name': 'always_search',
'default': False,
'advanced': True,
'type': 'bool',
'label': 'Always search',
'description': 'Search for movies even before there is a ETA. Enabling this will probably get you a lot of fakes.',
},
], ],
}, { }, {
'tab': 'searcher', 'tab': 'searcher',

47
couchpotato/core/plugins/searcher/main.py

@ -50,7 +50,12 @@ class Searcher(Plugin):
}"""}, }"""},
}) })
# Schedule cronjob addEvent('app.load', self.setCrons)
addEvent('setting.save.searcher.cron_day.after', self.setCrons)
addEvent('setting.save.searcher.cron_hour.after', self.setCrons)
addEvent('setting.save.searcher.cron_minute.after', self.setCrons)
def setCrons(self):
fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute')) fireEvent('schedule.cron', 'searcher.all', self.allMovies, day = self.conf('cron_day'), hour = self.conf('cron_hour'), minute = self.conf('cron_minute'))
def allMoviesView(self): def allMoviesView(self):
@ -141,8 +146,7 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True) pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True) release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True) available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
found_releases = [] found_releases = []
@ -157,7 +161,7 @@ class Searcher(Plugin):
ret = False ret = False
for quality_type in movie['profile']['types']: for quality_type in movie['profile']['types']:
if not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates): if not self.conf('always_search') and not self.couldBeReleased(quality_type['quality']['identifier'] in pre_releases, release_dates):
log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title)) log.info('Too early to search for %s, %s', (quality_type['quality']['identifier'], default_title))
continue continue
@ -285,10 +289,10 @@ class Searcher(Plugin):
if filedata == 'try_next': if filedata == 'try_next':
return filedata return filedata
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True) download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
if successful:
if download_result:
try: try:
# Mark release as snatched # Mark release as snatched
db = get_session() db = get_session()
@ -298,6 +302,15 @@ class Searcher(Plugin):
done_status = fireEvent('status.get', 'done', single = True) done_status = fireEvent('status.get', 'done', single = True)
rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id') rls.status_id = done_status.get('id') if not renamer_enabled else snatched_status.get('id')
# Save download-id info if returned
if isinstance(download_result, dict):
for key in download_result:
rls_info = ReleaseInfo(
identifier = 'download_%s' % key,
value = toUnicode(download_result.get(key))
)
rls.info.append(rls_info)
db.commit() db.commit()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label) log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@ -333,7 +346,7 @@ class Searcher(Plugin):
return True return True
log.info('Tried to download, but none of the "%s" downloaders are enabled', (data.get('type', ''))) log.info('Tried to download, but none of the "%s" downloaders are enabled or gave an error', (data.get('type', '')))
return False return False
@ -357,7 +370,7 @@ class Searcher(Plugin):
return search_types return search_types
def correctMovie(self, nzb = {}, movie = {}, quality = {}, **kwargs): def correctMovie(self, nzb = None, movie = None, quality = None, **kwargs):
imdb_results = kwargs.get('imdb_results', False) imdb_results = kwargs.get('imdb_results', False)
retention = Env.setting('retention', section = 'nzb') retention = Env.setting('retention', section = 'nzb')
@ -370,8 +383,9 @@ class Searcher(Plugin):
movie_words = re.split('\W+', simplifyString(movie_name)) movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(nzb['name']) nzb_name = simplifyString(nzb['name'])
nzb_words = re.split('\W+', nzb_name) nzb_words = re.split('\W+', nzb_name)
required_words = splitString(self.conf('required_words').lower())
# Make sure it has required words
required_words = splitString(self.conf('required_words').lower())
req_match = 0 req_match = 0
for req_set in required_words: for req_set in required_words:
req = splitString(req_set, '&') req = splitString(req_set, '&')
@ -381,19 +395,24 @@ class Searcher(Plugin):
log.info2("Wrong: Required word missing: %s" % nzb['name']) log.info2("Wrong: Required word missing: %s" % nzb['name'])
return False return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words').lower()) ignored_words = splitString(self.conf('ignored_words').lower())
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words)) ignored_match = 0
if self.conf('ignored_words') and blacklisted: for ignored_set in ignored_words:
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted))) ignored = splitString(ignored_set, '&')
ignored_match += len(list(set(nzb_words) & set(ignored))) == len(ignored)
if self.conf('ignored_words') and ignored_match:
log.info2("Wrong: '%s' contains 'ignored words'" % (nzb['name']))
return False return False
# Ignore porn stuff
pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic'] pron_tags = ['xxx', 'sex', 'anal', 'tits', 'fuck', 'porn', 'orgy', 'milf', 'boobs', 'erotica', 'erotic']
pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words)) pron_words = list(set(nzb_words) & set(pron_tags) - set(movie_words))
if pron_words: if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name'])) log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False return False
#qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True) preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string # Contains lower quality string

27
couchpotato/core/plugins/status/main.py

@ -25,13 +25,14 @@ class StatusPlugin(Plugin):
'available': 'Available', 'available': 'Available',
'suggest': 'Suggest', 'suggest': 'Suggest',
} }
status_cached = {}
def __init__(self): def __init__(self):
addEvent('status.add', self.add) addEvent('status.get', self.get)
addEvent('status.get', self.add) # Alias for .add
addEvent('status.get_by_id', self.getById) addEvent('status.get_by_id', self.getById)
addEvent('status.all', self.all) addEvent('status.all', self.all)
addEvent('app.initialize', self.fill) addEvent('app.initialize', self.fill)
addEvent('app.load', self.all) # Cache all statuses
addApiView('status.list', self.list, docs = { addApiView('status.list', self.list, docs = {
'desc': 'Check for available update', 'desc': 'Check for available update',
@ -67,12 +68,24 @@ class StatusPlugin(Plugin):
s = status.to_dict() s = status.to_dict()
temp.append(s) temp.append(s)
#db.close() # Update cache
self.status_cached[status.identifier] = s
return temp return temp
def add(self, identifier): def get(self, identifiers):
if not isinstance(identifiers, (list)):
identifiers = [identifiers]
db = get_session() db = get_session()
return_list = []
for identifier in identifiers:
if self.status_cached.get(identifier):
return_list.append(self.status_cached.get(identifier))
continue
s = db.query(Status).filter_by(identifier = identifier).first() s = db.query(Status).filter_by(identifier = identifier).first()
if not s: if not s:
@ -85,8 +98,10 @@ class StatusPlugin(Plugin):
status_dict = s.to_dict() status_dict = s.to_dict()
#db.close() self.status_cached[identifier] = status_dict
return status_dict return_list.append(status_dict)
return return_list if len(identifiers) > 1 else return_list[0]
def fill(self): def fill(self):

2
couchpotato/core/plugins/subtitle/__init__.py

@ -20,7 +20,7 @@ config = [{
}, },
{ {
'name': 'languages', 'name': 'languages',
'description': 'Comma separated, 2 letter country code. Example: en, nl', 'description': 'Comma separated, 2 letter country code. Example: en, nl. See the codes at <a href="http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes">on Wikipedia</a>',
}, },
# { # {
# 'name': 'automatic', # 'name': 'automatic',

4
couchpotato/core/plugins/trailer/__init__.py

@ -22,14 +22,14 @@ config = [{
'name': 'quality', 'name': 'quality',
'default': '720p', 'default': '720p',
'type': 'dropdown', 'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')], 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
}, },
{ {
'name': 'name', 'name': 'name',
'label': 'Naming', 'label': 'Naming',
'default': '<filename>-trailer', 'default': '<filename>-trailer',
'advanced': True, 'advanced': True,
'description': 'Use <filename> to use above settings.' 'description': 'Use <strong>&lt;filename&gt;</strong> to use above settings.'
}, },
], ],
}, },

9
couchpotato/core/plugins/trailer/main.py

@ -22,10 +22,15 @@ class Trailer(Plugin):
return False return False
for trailer in trailers.get(self.conf('quality'), []): for trailer in trailers.get(self.conf('quality'), []):
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % getExt(trailer))
ext = getExt(trailer)
filename = self.conf('name').replace('<filename>', group['filename']) + ('.%s' % ('mp4' if len(ext) > 5 else ext))
destination = os.path.join(group['destination_dir'], filename) destination = os.path.join(group['destination_dir'], filename)
if not os.path.isfile(destination): if not os.path.isfile(destination):
fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True) trailer_file = fireEvent('file.download', url = trailer, dest = destination, urlopen_kwargs = {'headers': {'User-Agent': 'Quicktime'}}, single = True)
if os.path.getsize(trailer_file) < (1024 * 1024): # Don't trust small trailers (1MB), try next one
os.unlink(trailer_file)
continue
else: else:
log.debug('Trailer already exists: %s', destination) log.debug('Trailer already exists: %s', destination)

24
couchpotato/core/plugins/userscript/static/userscript.css

@ -5,6 +5,7 @@
bottom: 0; bottom: 0;
left: 0; left: 0;
right: 0; right: 0;
padding: 0;
} }
.page.userscript .frame.loading { .page.userscript .frame.loading {
@ -12,3 +13,26 @@
font-size: 20px; font-size: 20px;
padding: 20px; padding: 20px;
} }
.page.userscript .movie_result {
height: 140px;
}
.page.userscript .movie_result .thumbnail {
width: 90px;
}
.page.userscript .movie_result .options {
left: 90px;
padding: 54px 15px;
}
.page.userscript .movie_result .year {
display: none;
}
.page.userscript .movie_result .options select[name="title"] {
width: 190px;
}
.page.userscript .movie_result .options select[name="profile"] {
width: 70px;
}

19
couchpotato/core/plugins/userscript/static/userscript.js

@ -63,28 +63,19 @@ var UserscriptSettingTab = new Class({
self.settings = App.getPage('Settings') self.settings = App.getPage('Settings')
self.settings.addEvent('create', function(){ self.settings.addEvent('create', function(){
// See if userscript can be installed
var userscript = false;
try {
if(Components.interfaces.gmIGreasemonkeyService)
userscript = true
}
catch(e){
userscript = Browser.chrome === true;
}
var host_url = window.location.protocol + '//' + window.location.host; var host_url = window.location.protocol + '//' + window.location.host;
self.settings.createGroup({ self.settings.createGroup({
'name': 'userscript', 'name': 'userscript',
'label': 'Install the bookmarklet' + (userscript ? ' or userscript' : ''), 'label': 'Install the bookmarklet or userscript',
'description': 'Easily add movies via imdb.com, appletrailers and more' 'description': 'Easily add movies via imdb.com, appletrailers and more'
}).inject(self.settings.tabs.automation.content, 'top').adopt( }).inject(self.settings.tabs.automation.content, 'top').adopt(
(userscript ? [new Element('a.userscript.button', { new Element('a.userscript.button', {
'text': 'Install userscript', 'text': 'Install userscript',
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js', 'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
'target': '_self' 'target': '_blank'
}), new Element('span.or[text=or]')] : null), }),
new Element('span.or[text=or]'),
new Element('span.bookmarklet').adopt( new Element('span.bookmarklet').adopt(
new Element('a.button.green', { new Element('a.button.green', {
'text': '+CouchPotato', 'text': '+CouchPotato',

5
couchpotato/core/plugins/userscript/template.js

@ -1,4 +1,9 @@
// ==UserScript== // ==UserScript==
//
// If you can read this, you need to enable or install the Greasemonkey add-on for firefox
// If you are using Chrome, download this file and drag it to the extensions tab
// Other browsers, use the bookmarklet
//
// @name CouchPotato UserScript // @name CouchPotato UserScript
// @description Add movies like a real CouchPotato // @description Add movies like a real CouchPotato
// @grant none // @grant none

55
couchpotato/core/plugins/wizard/static/wizard.css

@ -1,50 +1,61 @@
.page.wizard .uniForm { .page.wizard .uniForm {
width: 80%; margin: 0 0 30px;
margin: 0 auto 30px; width: 83%;
} }
.page.wizard h1 { .page.wizard h1 {
padding: 10px 30px; padding: 10px 0;
margin: 0; margin: 0 5px;
display: block; display: block;
font-size: 30px; font-size: 30px;
margin-top: 80px; margin-top: 80px;
} }
.page.wizard .description { .page.wizard .description {
padding: 10px 30px; padding: 10px 5px;
font-size: 18px; font-size: 1.45em;
line-height: 1.4em;
display: block; display: block;
} }
.page.wizard .tab_wrapper { .page.wizard .tab_wrapper {
background: #5c697b; background: #5c697b;
padding: 10px 0; height: 65px;
font-size: 18px; font-size: 1.75em;
position: fixed; position: fixed;
top: 0; top: 0;
margin: 0; margin: 0;
width: 100%; width: 100%;
min-width: 960px;
left: 0; left: 0;
z-index: 2; z-index: 2;
box-shadow: 0 0 50px rgba(0,0,0,0.55); box-shadow: 0 0 10px rgba(0,0,0,0.1);
} }
.page.wizard .tab_wrapper .tabs { .page.wizard .tab_wrapper .tabs {
text-align: center;
padding: 0; padding: 0;
margin: 0; margin: 0 auto;
display: block; display: block;
height: 100%;
width: 100%;
max-width: 960px;
} }
.page.wizard .tabs li { .page.wizard .tabs li {
display: inline-block; display: inline-block;
height: 100%;
} }
.page.wizard .tabs li a { .page.wizard .tabs li a {
padding: 20px 10px; padding: 20px 10px;
height: 100%;
display: block;
color: #FFF;
font-weight: normal;
border-bottom: 4px solid transparent;
} }
.page.wizard .tabs li:hover a { border-color: #047792; }
.page.wizard .tabs li.done a { border-color: #04bce6; }
.page.wizard .tab_wrapper .pointer { .page.wizard .tab_wrapper .pointer {
border-right: 10px solid transparent; border-right: 10px solid transparent;
border-left: 10px solid transparent; border-left: 10px solid transparent;
@ -61,27 +72,13 @@
.page.wizard form > div { .page.wizard form > div {
min-height: 300px; min-height: 300px;
} }
.page.wizard .wgroup_finish {
height: 300px;
}
.page.wizard .wgroup_finish h1 {
text-align: center;
}
.page.wizard .wgroup_finish .wizard_support,
.page.wizard .wgroup_finish .description {
font-size: 25px;
line-height: 120%;
margin: 20px 0;
text-align: center;
}
.page.wizard .button.green { .page.wizard .button.green {
padding: 20px; padding: 20px;
font-size: 25px; font-size: 25px;
margin: 10px 30px 80px; margin: 10px 0 80px;
display: block; display: block;
text-align: center; }
}
.page.wizard .tab_nzb_providers { .page.wizard .tab_nzb_providers {
margin: 20px 0 0 0; margin: 20px 0 0 0;

37
couchpotato/core/plugins/wizard/static/wizard.js

@ -9,27 +9,12 @@ Page.Wizard = new Class({
headers: { headers: {
'welcome': { 'welcome': {
'title': 'Welcome to the new CouchPotato', 'title': 'Welcome to the new CouchPotato',
'description': 'To get started, fill in each of the following settings as much as you can. <br />Maybe first start with importing your movies from the previous CouchPotato', 'description': 'To get started, fill in each of the following settings as much as you can.',
'content': new Element('div', { 'content': new Element('div', {
'styles': { 'styles': {
'margin': '0 0 0 30px' 'margin': '0 0 0 30px'
} }
}).adopt(
new Element('div', {
'html': 'Select the <strong>data.db</strong>. It should be in your CouchPotato root directory.'
}),
self.import_iframe = new Element('iframe', {
'styles': {
'height': 40,
'width': 300,
'border': 0,
'overflow': 'hidden'
}
}) })
),
'event': function(){
self.import_iframe.set('src', Api.createUrl('v1.import'))
}
}, },
'general': { 'general': {
'title': 'General', 'title': 'General',
@ -178,7 +163,7 @@ Page.Wizard = new Class({
'href': App.createUrl('wizard/'+group), 'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize() 'text': (self.headers[group].label || group).capitalize()
}) })
).inject(tabs); ).inject(tabs)
} }
else else
@ -214,13 +199,7 @@ Page.Wizard = new Class({
self.el.getElement('.t_searcher').hide(); self.el.getElement('.t_searcher').hide();
// Add pointer // Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt( new Element('.tab_wrapper').wraps(tabs);
self.pointer = new Element('.pointer', {
'tween': {
'transition': 'quint:in:out'
}
})
);
// Add nav // Add nav
var minimum = self.el.getSize().y-window.getSize().y; var minimum = self.el.getSize().y-window.getSize().y;
@ -232,16 +211,18 @@ Page.Wizard = new Class({
if(!t) return; if(!t) return;
var func = function(){ var func = function(){
var ct = t.getCoordinates(); // Activate all previous ones
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2)); self.groups.each(function(groups2, nr2){
var t2 = self.el.getElement('.t_'+groups2);
t2[nr2 > nr ? 'removeClass' : 'addClass' ]('done');
})
g.tween('opacity', 1); g.tween('opacity', 1);
} }
if(nr == 0) if(nr == 0)
func(); func();
new ScrollSpy( {
var ss = new ScrollSpy( {
min: function(){ min: function(){
var c = g.getCoordinates(); var c = g.getCoordinates();
var top = c.top-(window.getSize().y/2); var top = c.top-(window.getSize().y/2);

22
couchpotato/core/providers/automation/base.py

@ -2,6 +2,7 @@ from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider from couchpotato.core.providers.base import Provider
from couchpotato.environment import Env from couchpotato.environment import Env
from couchpotato.core.helpers.variable import splitString
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@ -59,7 +60,26 @@ class Automation(Provider):
type_value = movie.get(minimal_type, 0) type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type) type_min = self.getMinimal(minimal_type)
if type_value < type_min: if type_value < type_min:
log.info('%s too low for %s, need %s has %s', (minimal_type, movie['imdb'], type_min, type_value)) log.info('%s too low for %s, need %s has %s', (minimal_type, movie['original_title'], type_min, type_value))
return False
movie_genres = [genre.lower() for genre in movie['genres']]
required_genres = splitString(self.getMinimal('required_genres').lower())
ignored_genres = splitString(self.getMinimal('ignored_genres').lower())
req_match = 0
for req_set in required_genres:
req = splitString(req_set, '&')
req_match += len(list(set(movie_genres) & set(req))) == len(req)
if self.getMinimal('required_genres') and req_match == 0:
log.info2("Required genre(s) missing for %s" % movie['original_title'])
return False
for ign_set in ignored_genres:
ign = splitString(ign_set, '&')
if len(list(set(movie_genres) & set(ign))) == len(ign):
log.info2("%s has blacklisted genre(s): %s" % (movie['original_title'], ign))
return False return False
return True return True

34
couchpotato/core/providers/automation/letterboxd/__init__.py

@ -0,0 +1,34 @@
from .main import Letterboxd
def start():
return Letterboxd()
config = [{
'name': 'letterboxd',
'groups': [
{
'tab': 'automation',
'list': 'watchlist_providers',
'name': 'letterboxd_automation',
'label': 'Letterboxd',
'description': 'Import movies from any public <a href="http://letterboxd.com/">Letterboxd</a> watchlist',
'options': [
{
'name': 'automation_enabled',
'default': False,
'type': 'enabler',
},
{
'name': 'automation_urls_use',
'label': 'Use',
},
{
'name': 'automation_urls',
'label': 'Username',
'type': 'combined',
'combine': ['automation_urls_use', 'automation_urls'],
},
],
},
],
}]

49
couchpotato/core/providers/automation/letterboxd/main.py

@ -0,0 +1,49 @@
from bs4 import BeautifulSoup
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.automation.base import Automation
import re
log = CPLog(__name__)
class Letterboxd(Automation):
url = 'http://letterboxd.com/%s/watchlist/'
pattern = re.compile(r'(.*)\((\d*)\)')
def getIMDBids(self):
urls = splitString(self.conf('automation_urls'))
if len(urls) == 0:
return []
movies = []
for movie in self.getWatchlist():
imdb_id = self.search(movie.get('title'), movie.get('year'), imdb_only = True)
movies.append(imdb_id)
return movies
def getWatchlist(self):
enablers = [tryInt(x) for x in splitString(self.conf('automation_urls_use'))]
urls = splitString(self.conf('automation_urls'))
index = -1
movies = []
for username in urls:
index += 1
if not enablers[index]:
continue
soup = BeautifulSoup(self.getHTMLData(self.url % username))
for movie in soup.find_all('a', attrs = { 'class': 'frame' }):
match = filter(None, self.pattern.split(movie['title']))
movies.append({'title': match[0], 'year': match[1] })
return movies

1
couchpotato/core/providers/base.py

@ -104,6 +104,7 @@ class YarrProvider(Provider):
try: try:
cookiejar = cookielib.CookieJar() cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar)) opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
opener.addheaders = []
urllib2.install_opener(opener) urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login']) log.info2('Logging into %s', self.urls['login'])
f = opener.open(self.urls['login'], self.getLoginParams()) f = opener.open(self.urls['login'], self.getLoginParams())

3
couchpotato/core/providers/movie/_modifier/main.py

@ -52,8 +52,7 @@ class MovieResultModifier(Plugin):
if l: if l:
# Statuses # Statuses
active_status = fireEvent('status.get', 'active', single = True) active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
done_status = fireEvent('status.get', 'done', single = True)
for movie in l.movies: for movie in l.movies:
if movie.status_id == active_status['id']: if movie.status_id == active_status['id']:

25
couchpotato/core/providers/movie/couchpotatoapi/main.py

@ -2,9 +2,11 @@ from couchpotato import get_session
from couchpotato.core.event import addEvent, fireEvent from couchpotato.core.event import addEvent, fireEvent
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
import time import time
log = CPLog(__name__) log = CPLog(__name__)
@ -18,19 +20,37 @@ class CouchPotatoApi(MovieProvider):
'is_movie': 'https://couchpota.to/api/ismovie/%s/', 'is_movie': 'https://couchpota.to/api/ismovie/%s/',
'eta': 'https://couchpota.to/api/eta/%s/', 'eta': 'https://couchpota.to/api/eta/%s/',
'suggest': 'https://couchpota.to/api/suggest/', 'suggest': 'https://couchpota.to/api/suggest/',
'updater': 'https://couchpota.to/api/updater/?%s',
'messages': 'https://couchpota.to/api/messages/?%s',
} }
http_time_between_calls = 0 http_time_between_calls = 0
api_version = 1 api_version = 1
def __init__(self): def __init__(self):
#addApiView('movie.suggest', self.suggestView)
addEvent('movie.info', self.getInfo, priority = 1) addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1) addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate) addEvent('movie.release_date', self.getReleaseDate)
addEvent('movie.suggest', self.suggest) addEvent('movie.suggest', self.suggest)
addEvent('movie.is_movie', self.isMovie) addEvent('movie.is_movie', self.isMovie)
addEvent('cp.source_url', self.getSourceUrl)
addEvent('cp.messages', self.getMessages)
def getMessages(self, last_check = 0):
data = self.getJsonData(self.urls['messages'] % tryUrlencode({
'last_check': last_check,
}), headers = self.getRequestHeaders(), cache_timeout = 10)
return data
def getSourceUrl(self, repo = None, repo_name = None, branch = None):
return self.getJsonData(self.urls['updater'] % tryUrlencode({
'repo': repo,
'name': repo_name,
'branch': branch,
}), headers = self.getRequestHeaders())
def search(self, q, limit = 12): def search(self, q, limit = 12):
return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders()) return self.getJsonData(self.urls['search'] % tryUrlencode(q), headers = self.getRequestHeaders())
@ -96,4 +116,5 @@ class CouchPotatoApi(MovieProvider):
'X-CP-Version': fireEvent('app.version', single = True), 'X-CP-Version': fireEvent('app.version', single = True),
'X-CP-API': self.api_version, 'X-CP-API': self.api_version,
'X-CP-Time': time.time(), 'X-CP-Time': time.time(),
'X-CP-Identifier': '+%s' % Env.setting('api_key', 'core')[:10], # Use first 10 as identifier, so we don't need to use IP address in api stats
} }

8
couchpotato/core/providers/nzb/binsearch/__init__.py

@ -18,6 +18,14 @@ config = [{
'name': 'enabled', 'name': 'enabled',
'type': 'enabler', 'type': 'enabler',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/nzb/ftdworld/__init__.py

@ -27,6 +27,14 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

20
couchpotato/core/providers/nzb/newznab/__init__.py

@ -12,9 +12,10 @@ config = [{
'list': 'nzb_providers', 'list': 'nzb_providers',
'name': 'newznab', 'name': 'newznab',
'order': 10, 'order': 10,
'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab providers</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \ 'description': 'Enable <a href="http://newznab.com/" target="_blank">NewzNab</a> such as <a href="https://nzb.su" target="_blank">NZB.su</a>, \
<a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \ <a href="https://nzbs.org" target="_blank">NZBs.org</a>, <a href="http://dognzb.cr/" target="_blank">DOGnzb.cr</a>, \
<a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a> or <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>', <a href="https://github.com/spotweb/spotweb" target="_blank">Spotweb</a>, <a href="https://nzbgeek.info/" target="_blank">NZBGeek</a>, \
<a href="https://smackdownonyou.com" target="_blank">SmackDown</a>, <a href="https://www.nzbfinder.ws" target="_blank">NZBFinder</a>',
'wizard': True, 'wizard': True,
'options': [ 'options': [
{ {
@ -23,20 +24,27 @@ config = [{
}, },
{ {
'name': 'use', 'name': 'use',
'default': '0,0,0,0' 'default': '0,0,0,0,0,0'
}, },
{ {
'name': 'host', 'name': 'host',
'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info', 'default': 'nzb.su,dognzb.cr,nzbs.org,https://index.nzbgeek.info, https://smackdownonyou.com, https://www.nzbfinder.ws',
'description': 'The hostname of your newznab provider', 'description': 'The hostname of your newznab provider',
}, },
{ {
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': '0,0,0,0,0,0',
'description': 'Starting score for each release found via this provider.',
},
{
'name': 'api_key', 'name': 'api_key',
'default': ',,,', 'default': ',,,,,',
'label': 'Api Key', 'label': 'Api Key',
'description': 'Can be found on your profile page', 'description': 'Can be found on your profile page',
'type': 'combined', 'type': 'combined',
'combine': ['use', 'host', 'api_key'], 'combine': ['use', 'host', 'api_key', 'extra_score'],
}, },
], ],
}, },

7
couchpotato/core/providers/nzb/newznab/main.py

@ -1,6 +1,6 @@
from couchpotato.core.helpers.encoding import tryUrlencode from couchpotato.core.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.rss import RSS from couchpotato.core.helpers.rss import RSS
from couchpotato.core.helpers.variable import cleanHost, splitString from couchpotato.core.helpers.variable import cleanHost, splitString, tryInt
from couchpotato.core.logger import CPLog from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import ResultList from couchpotato.core.providers.base import ResultList
from couchpotato.core.providers.nzb.base import NZBProvider from couchpotato.core.providers.nzb.base import NZBProvider
@ -76,6 +76,7 @@ class Newznab(NZBProvider, RSS):
'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host), 'url': (self.getUrl(host['host'], self.urls['download']) % tryUrlencode(nzb_id)) + self.getApiExt(host),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)), 'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)),
'content': self.getTextElement(nzb, 'description'), 'content': self.getTextElement(nzb, 'description'),
'score': host['extra_score'],
}) })
def getHosts(self): def getHosts(self):
@ -83,13 +84,15 @@ class Newznab(NZBProvider, RSS):
uses = splitString(str(self.conf('use'))) uses = splitString(str(self.conf('use')))
hosts = splitString(self.conf('host')) hosts = splitString(self.conf('host'))
api_keys = splitString(self.conf('api_key')) api_keys = splitString(self.conf('api_key'))
extra_score = splitString(self.conf('extra_score'))
list = [] list = []
for nr in range(len(hosts)): for nr in range(len(hosts)):
list.append({ list.append({
'use': uses[nr], 'use': uses[nr],
'host': hosts[nr], 'host': hosts[nr],
'api_key': api_keys[nr] 'api_key': api_keys[nr],
'extra_score': tryInt(extra_score[nr]) if len(extra_score) > nr else 0
}) })
return list return list

8
couchpotato/core/providers/nzb/nzbclub/__init__.py

@ -18,6 +18,14 @@ config = [{
'name': 'enabled', 'name': 'enabled',
'type': 'enabler', 'type': 'enabler',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/nzb/nzbindex/__init__.py

@ -19,6 +19,14 @@ config = [{
'type': 'enabler', 'type': 'enabler',
'default': True, 'default': True,
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/nzb/nzbsrus/__init__.py

@ -35,6 +35,14 @@ config = [{
'label': 'English only', 'label': 'English only',
'description': 'Only search for English spoken movies on Nzbsrus', 'description': 'Only search for English spoken movies on Nzbsrus',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/nzb/nzbx/__init__.py

@ -19,6 +19,14 @@ config = [{
'type': 'enabler', 'type': 'enabler',
'default': True, 'default': True,
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/nzb/omgwtfnzbs/__init__.py

@ -27,6 +27,14 @@ config = [{
'label': 'Api Key', 'label': 'Api Key',
'default': '', 'default': '',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'default': 20,
'type': 'int',
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/iptorrents/__init__.py

@ -34,6 +34,14 @@ config = [{
'type': 'bool', 'type': 'bool',
'description': 'Only search for [FreeLeech] torrents.', 'description': 'Only search for [FreeLeech] torrents.',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/kickasstorrents/__init__.py

@ -19,6 +19,14 @@ config = [{
'type': 'enabler', 'type': 'enabler',
'default': True, 'default': True,
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/passthepopcorn/__init__.py

@ -37,6 +37,14 @@ config = [{
{ {
'name': 'passkey', 'name': 'passkey',
'default': '', 'default': '',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
} }
], ],
} }

8
couchpotato/core/providers/torrent/publichd/__init__.py

@ -19,6 +19,14 @@ config = [{
'type': 'enabler', 'type': 'enabler',
'default': True, 'default': True,
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/sceneaccess/__init__.py

@ -28,6 +28,14 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/scenehd/__init__.py

@ -28,6 +28,14 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/thepiratebay/__init__.py

@ -24,6 +24,14 @@ config = [{
'advanced': True, 'advanced': True,
'label': 'Proxy server', 'label': 'Proxy server',
'description': 'Domain for requests, keep empty to let CouchPotato pick.', 'description': 'Domain for requests, keep empty to let CouchPotato pick.',
},
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
} }
], ],
} }

8
couchpotato/core/providers/torrent/torrentday/__init__.py

@ -28,6 +28,14 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 0,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

8
couchpotato/core/providers/torrent/torrentleech/__init__.py

@ -28,6 +28,14 @@ config = [{
'default': '', 'default': '',
'type': 'password', 'type': 'password',
}, },
{
'name': 'extra_score',
'advanced': True,
'label': 'Extra Score',
'type': 'int',
'default': 20,
'description': 'Starting score for each release found via this provider.',
}
], ],
}, },
], ],

3
couchpotato/core/providers/torrent/torrentleech/main.py

@ -74,3 +74,6 @@ class TorrentLeech(TorrentProvider):
'remember_me': 'on', 'remember_me': 'on',
'login': 'submit', 'login': 'submit',
}) })
def loginSuccess(self, output):
return '/user/account/logout' in output.lower() or 'welcome back' in output.lower()

6
couchpotato/core/providers/userscript/criticker/__init__.py

@ -0,0 +1,6 @@
from .main import Criticker
def start():
return Criticker()
config = []

6
couchpotato/core/providers/userscript/criticker/main.py

@ -0,0 +1,6 @@
from couchpotato.core.providers.userscript.base import UserscriptBase
class Criticker(UserscriptBase):
includes = ['http://www.criticker.com/film/*']

3
couchpotato/core/settings/__init__.py

@ -189,6 +189,9 @@ class Settings(object):
self.set(section, option, (new_value if new_value else value).encode('unicode_escape')) self.set(section, option, (new_value if new_value else value).encode('unicode_escape'))
self.save() self.save()
# After save (for re-interval etc)
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
return jsonified({ return jsonified({
'success': True, 'success': True,
}) })

26
couchpotato/core/settings/model.py

@ -76,7 +76,7 @@ class LibraryTitle(Entity):
title = Field(Unicode) title = Field(Unicode)
simple_title = Field(Unicode, index = True) simple_title = Field(Unicode, index = True)
default = Field(Boolean, index = True) default = Field(Boolean, default = False, index = True)
language = OneToMany('Language') language = OneToMany('Language')
libraries = ManyToOne('Library') libraries = ManyToOne('Library')
@ -141,12 +141,12 @@ class Status(Entity):
class Quality(Entity): class Quality(Entity):
"""Quality name of a release, DVD, 720P, DVD-Rip etc""" """Quality name of a release, DVD, 720p, DVD-Rip etc"""
using_options(order_by = 'order') using_options(order_by = 'order')
identifier = Field(String(20), unique = True) identifier = Field(String(20), unique = True)
label = Field(Unicode(20)) label = Field(Unicode(20))
order = Field(Integer, index = True) order = Field(Integer, default = 0, index = True)
size_min = Field(Integer) size_min = Field(Integer)
size_max = Field(Integer) size_max = Field(Integer)
@ -160,21 +160,27 @@ class Profile(Entity):
using_options(order_by = 'order') using_options(order_by = 'order')
label = Field(Unicode(50)) label = Field(Unicode(50))
order = Field(Integer, index = True) order = Field(Integer, default = 0, index = True)
core = Field(Boolean) core = Field(Boolean, default = False)
hide = Field(Boolean) hide = Field(Boolean, default = False)
movie = OneToMany('Movie') movie = OneToMany('Movie')
types = OneToMany('ProfileType', cascade = 'all, delete-orphan') types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
def to_dict(self, deep = {}, exclude = []):
orig_dict = super(Profile, self).to_dict(deep = deep, exclude = exclude)
orig_dict['core'] = orig_dict.get('core') or False
orig_dict['hide'] = orig_dict.get('hide') or False
return orig_dict
class ProfileType(Entity): class ProfileType(Entity):
"""""" """"""
using_options(order_by = 'order') using_options(order_by = 'order')
order = Field(Integer, index = True) order = Field(Integer, default = 0, index = True)
finish = Field(Boolean) finish = Field(Boolean, default = True)
wait_for = Field(Integer) wait_for = Field(Integer, default = 0)
quality = ManyToOne('Quality') quality = ManyToOne('Quality')
profile = ManyToOne('Profile') profile = ManyToOne('Profile')
@ -185,7 +191,7 @@ class File(Entity):
path = Field(Unicode(255), nullable = False, unique = True) path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer, default = 1) part = Field(Integer, default = 1)
available = Field(Boolean) available = Field(Boolean, default = True)
type = ManyToOne('FileType') type = ManyToOne('FileType')
properties = OneToMany('FileProperty') properties = OneToMany('FileProperty')

8
couchpotato/runner.py

@ -190,7 +190,10 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
version_control(db, repo, version = latest_db_version) version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo) current_db_version = db_version(db, repo)
if current_db_version < latest_db_version and not development: if current_db_version < latest_db_version:
if development:
log.error('There is a database migration ready, but you are running development mode, so it won\'t be used. If you see this, you are stupid. Please disable development mode.')
else:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version)) log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo) upgrade(db, repo)
@ -238,7 +241,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from tornado.ioloop import IOLoop from tornado.ioloop import IOLoop
web_container = WSGIContainer(app) web_container = WSGIContainer(app)
web_container._log = _log web_container._log = _log
loop = IOLoop.instance() loop = IOLoop.current()
application = Application([ application = Application([
(r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler), (r'%s/api/%s/nonblock/(.*)/' % (url_base, api_key), NonBlockHandler),

BIN
couchpotato/static/fonts/Elusive-Icons.eot

Binary file not shown.

298
couchpotato/static/fonts/Elusive-Icons.svg

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 213 KiB

BIN
couchpotato/static/fonts/Elusive-Icons.ttf

Binary file not shown.

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

Loading…
Cancel
Save