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. 13
      couchpotato/core/_base/clientscript/main.py
  5. 46
      couchpotato/core/_base/scheduler/main.py
  6. 30
      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. 48
      couchpotato/core/downloaders/sabnzbd/main.py
  13. 11
      couchpotato/core/downloaders/transmission/__init__.py
  14. 130
      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. 78
      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. 204
      couchpotato/core/plugins/movie/static/list.js
  44. 98
      couchpotato/core/plugins/movie/static/movie.actions.js
  45. 368
      couchpotato/core/plugins/movie/static/movie.css
  46. 43
      couchpotato/core/plugins/movie/static/movie.js
  47. 236
      couchpotato/core/plugins/movie/static/search.css
  48. 83
      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. 40
      couchpotato/core/plugins/release/main.py
  53. 11
      couchpotato/core/plugins/renamer/__init__.py
  54. 213
      couchpotato/core/plugins/renamer/main.py
  55. 65
      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. 45
      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. 61
      couchpotato/core/plugins/wizard/static/wizard.css
  67. 39
      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. 12
      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):
def __init__(self, application, request, **kwargs):
cls = NonBlockHandler
cls.stoppers = []
super(NonBlockHandler, self).__init__(application, request, **kwargs)
stoppers = []
@asynchronous
def get(self, route):
cls = NonBlockHandler
start, stop = api_nonblock[route]
cls.stoppers.append(stop)
self.stoppers.append(stop)
start(self.onNewMessage, last_id = self.get_argument("last_id", None))
@ -30,12 +26,11 @@ class NonBlockHandler(RequestHandler):
self.finish(response)
def on_connection_close(self):
cls = NonBlockHandler
for stop in cls.stoppers:
for stop in self.stoppers:
stop(self.onNewMessage)
cls.stoppers = []
self.stoppers = []
def addApiView(route, func, static = False, docs = None, **kwargs):

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

@ -70,7 +70,7 @@ config = [{
'name': 'development',
'default': 0,
'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',

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

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

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

@ -1,10 +1,11 @@
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.encoding import ss
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.plugins.base import Plugin
from couchpotato.environment import Env
from minify.cssmin import cssmin
from minify.jsmin import jsmin
import cssprefixer
import os
import traceback
@ -23,7 +24,6 @@ class ClientScript(Plugin):
'script': [
'scripts/library/mootools.js',
'scripts/library/mootools_more.js',
'scripts/library/prefix_free.js',
'scripts/library/uniform.js',
'scripts/library/form_replacement/form_check.js',
'scripts/library/form_replacement/form_radio.js',
@ -69,7 +69,8 @@ class ClientScript(Plugin):
addEvent('clientscript.get_styles', self.getStyles)
addEvent('clientscript.get_scripts', self.getScripts)
addEvent('app.load', self.minify)
if not Env.get('dev'):
addEvent('app.load', self.minify)
self.addCore()
@ -108,8 +109,10 @@ class ClientScript(Plugin):
if file_type == 'script':
data = jsmin(f)
else:
data = cssmin(f)
data = cssprefixer.process(f, debug = False, minify = True)
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})
@ -119,7 +122,7 @@ class ClientScript(Plugin):
data += self.comment.get(file_type) % (r.get('file'), r.get('date'))
data += r.get('data') + '\n\n'
self.createFile(out, data.strip())
self.createFile(out, ss(data.strip()))
if not self.minified.get(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.interval', self.interval)
addEvent('schedule.start', self.start)
addEvent('schedule.restart', self.start)
addEvent('app.load', self.start)
addEvent('schedule.remove', self.remove)
self.sched = Sched(misfire_grace_time = 60)
self.sched.start()
self.started = True
def remove(self, identifier):
for type in ['interval', 'cron']:
for cron_type in ['intervals', 'crons']:
try:
self.sched.unschedule_job(getattr(self, type)[identifier]['job'])
log.debug('%s unscheduled %s', (type.capitalize(), identifier))
self.sched.unschedule_job(getattr(self, cron_type)[identifier]['job'])
log.debug('%s unscheduled %s', (cron_type.capitalize(), identifier))
except:
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):
super(Scheduler, self).doShutdown()
self.stop()
@ -82,6 +50,7 @@ class Scheduler(Plugin):
'day': day,
'hour': hour,
'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):
@ -93,4 +62,5 @@ class Scheduler(Plugin):
'hours': hours,
'minutes': minutes,
'seconds': seconds,
'job': self.sched.add_interval_job(handle, hours = hours, minutes = minutes, seconds = seconds)
}

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

@ -15,6 +15,7 @@ import tarfile
import time
import traceback
import version
import zipfile
log = CPLog(__name__)
@ -32,7 +33,6 @@ class Updater(Plugin):
else:
self.updater = SourceUpdater()
fireEvent('schedule.interval', 'updater.check', self.autoUpdate, hours = 6)
addEvent('app.load', self.autoUpdate)
addEvent('updater.info', self.info)
@ -52,6 +52,15 @@ class Updater(Plugin):
'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):
if self.check() and self.conf('automatic') and not self.updater.update_failed:
if self.updater.doUpdate():
@ -255,11 +264,11 @@ class SourceUpdater(BaseUpdater):
def doUpdate(self):
try:
url = 'https://github.com/%s/%s/tarball/%s' % (self.repo_user, self.repo_name, self.branch)
destination = os.path.join(Env.get('cache_dir'), self.update_version.get('hash') + '.tar.gz')
extracted_path = os.path.join(Env.get('cache_dir'), 'temp_updater')
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')) + '.' + download_data.get('type')
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
if os.path.isdir(extracted_path):
@ -267,9 +276,14 @@ class SourceUpdater(BaseUpdater):
self.makeDir(extracted_path)
# Extract
tar = tarfile.open(destination)
tar.extractall(path = extracted_path)
tar.close()
if download_data.get('type') == 'zip':
zip = zipfile.ZipFile(destination)
zip.extractall(extracted_path)
else:
tar = tarfile.open(destination)
tar.extractall(path = extracted_path)
tar.close()
os.remove(destination)
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(){
var self = this;
App.addEvent('load', self.info.bind(self, 1000))
App.addEvent('load', self.info.bind(self, 2000))
App.addEvent('unload', function(){
if(self.timer)
clearTimeout(self.timer);
@ -84,7 +84,7 @@ var UpdaterBase = new Class({
'click': self.doUpdate.bind(self)
}
})
).inject($(document.body).getElement('.header'))
).inject(document.body)
},
doUpdate: function(){

39
couchpotato/core/downloaders/base.py

@ -1,5 +1,6 @@
from base64 import b32decode, b16encode
from couchpotato.core.event import addEvent
from couchpotato.core.helpers.variable import mergeDicts
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.base import Provider
import random
@ -103,6 +104,12 @@ class Downloader(Provider):
log.error('Failed converting magnet url to torrent: %s', (torrent_hash))
return False
def downloadReturnId(self, download_id):
return {
'downloader': self.getName(),
'id': download_id
}
def isDisabled(self, manual, data):
return not self.isEnabled(manual, data)
@ -116,3 +123,35 @@ class Downloader(Provider):
return super(Downloader, self).isEnabled() and \
((d_manual and manual) or (d_manual is False)) and \
(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>',
},
{
'name': 'username',
'default': 'nzbget',
'advanced': True,
'description': 'Set a different username to connect. Default: nzbget',
},
{
'name': 'password',
'type': 'password',
'description': 'Default NZBGet password is <i>tegbzn6789</i>',
@ -48,6 +54,12 @@ config = [{
'advanced': True,
'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 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.variable import tryInt
from couchpotato.core.helpers.variable import tryInt, md5
from couchpotato.core.logger import CPLog
from datetime import timedelta
import re
import shutil
import socket
import traceback
import xmlrpclib
@ -14,7 +16,7 @@ class NZBGet(Downloader):
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):
@ -24,7 +26,7 @@ class NZBGet(Downloader):
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))
rpc = xmlrpclib.ServerProxy(url)
@ -50,7 +52,124 @@ class NZBGet(Downloader):
if xml_response:
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:
log.error('NZBGet could not add %s to the queue.', nzb_name)
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 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.variable import cleanHost
from couchpotato.core.logger import CPLog
@ -29,7 +29,9 @@ class NZBVortex(Downloader):
nzb_filename = self.createFileName(data, filedata, movie)
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:
log.error('Something went wrong sending the NZB file: %s', traceback.format_exc())
return False
@ -38,7 +40,7 @@ class NZBVortex(Downloader):
raw_statuses = self.call('nzb')
statuses = []
statuses = StatusList(self)
for item in raw_statuses.get('nzbs', []):
# Check status
@ -53,7 +55,8 @@ class NZBVortex(Downloader):
'name': item['uiTitle'],
'status': status,
'original_status': item['state'],
'timeleft':-1,
'timeleft': -1,
'folder': item['destinationPath'],
})
return statuses

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

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

@ -41,16 +41,23 @@ config = [{
{
'name': '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',
'default': 10,
'type': 'int',
'type': 'float',
'advanced': True,
'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',
'default': 0,
'type': 'bool',

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

@ -1,11 +1,15 @@
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.logger import CPLog
from couchpotato.environment import Env
from datetime import timedelta
import httplib
import json
import os.path
import re
import shutil
import traceback
import urllib2
log = CPLog(__name__)
@ -18,7 +22,7 @@ class Transmission(Downloader):
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.
host = self.conf('host').split(':')
@ -27,22 +31,24 @@ class Transmission(Downloader):
return False
# Set parameters for Transmission
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)
# Create the empty folder to download too
self.makeDir(folder_path)
params = {
'paused': self.conf('paused', default = 0),
'download-dir': folder_path
}
if len(self.conf('directory', default = '')) > 0:
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)
# Create the empty folder to download too
self.makeDir(folder_path)
params['download-dir'] = folder_path
torrent_params = {}
if self.conf('ratio'):
torrent_params = {
'seedRatioLimit': self.conf('ratio'),
'seedRatioMode': self.conf('ratio')
'seedRatioMode': self.conf('ratiomode')
}
if not filedata and data.get('type') == 'torrent':
@ -58,15 +64,97 @@ class Transmission(Downloader):
else:
remote_torrent = trpc.add_torrent_file(b64encode(filedata), arguments = params)
if not remote_torrent:
return False
# Change settings of added torrents
if torrent_params:
elif 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:
log.error('Failed to change settings for transfer: %s', err)
log.error('Failed getting queue: %s', err)
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):
@ -97,6 +185,7 @@ class TransmissionRPC(object):
try:
open_request = urllib2.urlopen(request)
response = json.loads(open_request.read())
log.debug('request: %s', json.dumps(ojson))
log.debug('response: %s', json.dumps(response))
if response['result'] == 'success':
log.debug('Transmission action successfull')
@ -146,3 +235,18 @@ class TransmissionRPC(object):
arguments['ids'] = torrent_id
post_data = {'arguments': arguments, 'method': 'torrent-set', 'tag': self.tag}
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 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.logger import CPLog
from hashlib import sha1
from multipartpost import MultipartPostHandler
from datetime import timedelta
import os
import cookielib
import httplib
import json
@ -66,7 +68,7 @@ class uTorrent(Downloader):
self.utorrent_api.set_torrent(torrent_hash, torrent_params)
if self.conf('paused', default = 0):
self.utorrent_api.pause_torrent(torrent_hash)
return True
return self.downloadReturnId(torrent_hash)
except Exception, err:
log.error('Failed to send torrent to uTorrent: %s', err)
return False
@ -103,7 +105,36 @@ class uTorrent(Downloader):
log.debug('Nothing in queue')
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
for item in queue.get('torrents', []):
@ -113,12 +144,18 @@ class uTorrent(Downloader):
if item[21] == 'Finished' or item[21] == 'Seeding':
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({
'id': item[0],
'name': item[2],
'status': status,
'original_status': item[1],
'timeleft': item[10],
'timeleft': str(timedelta(seconds = item[10])),
'folder': release_folder,
})
return statuses
@ -195,3 +232,7 @@ class uTorrentAPI(object):
def get_status(self):
action = "list=1"
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):
if events.get(name):
e = events[name]
else:
e = events[name] = Event(name = name, threads = 10, exc_info = True, traceback = True, lock = threading.RLock())
if not events.get(name):
events[name] = []
def createHandle(*args, **kwargs):
@ -35,7 +33,10 @@ def addEvent(name, handler, priority = 100):
return h
e.handle(createHandle, priority = priority)
events[name].append({
'handler': createHandle,
'priority': priority,
})
def removeEvent(name, handler):
e = events[name]
@ -43,6 +44,12 @@ def removeEvent(name, handler):
def fireEvent(name, *args, **kwargs):
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)
try:
@ -52,6 +59,7 @@ def fireEvent(name, *args, **kwargs):
'single': False, # Return single handler
'merge': False, # Merge items
'in_order': False, # Fire them in specific order, waits for the other to finish
'async': False
}
# Do options
@ -62,13 +70,6 @@ def fireEvent(name, *args, **kwargs):
options[x] = val
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
kwargs['event_order_lock'] = threading.RLock() if options['in_order'] or options['single'] else None
kwargs['event_return_on_result'] = options['single']
@ -76,9 +77,6 @@ def fireEvent(name, *args, **kwargs):
# Fire
result = e(*args, **kwargs)
# Release lock for this event
e.lock.release()
if options['single'] and not options['merge']:
results = None
@ -104,13 +102,14 @@ def fireEvent(name, *args, **kwargs):
# Merge
if options['merge'] and len(results) > 0:
results.reverse() # Priority 1 is higher then 100
# Dict
if isinstance(results[0], dict):
results.reverse()
merged = {}
for result in results:
merged = mergeDicts(merged, result)
merged = mergeDicts(merged, result, prepend_list = True)
results = merged
# Lists
@ -140,13 +139,8 @@ def fireEvent(name, *args, **kwargs):
log.error('%s: %s', (name, traceback.format_exc()))
def fireEventAsync(*args, **kwargs):
try:
my_thread = threading.Thread(target = fireEvent, args = args, kwargs = kwargs)
my_thread.setDaemon(True)
my_thread.start()
return True
except Exception, e:
log.error('%s: %s', (args[0], e))
kwargs['async'] = True
fireEvent(*args, **kwargs)
def errorHandler(error):
etype, value, tb = error

18
couchpotato/core/helpers/variable.py

@ -10,6 +10,20 @@ import sys
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():
try:
import pwd
@ -53,7 +67,7 @@ def getDataDir():
def isDict(object):
return isinstance(object, dict)
def mergeDicts(a, b):
def mergeDicts(a, b, prepend_list = False):
assert isDict(a), isDict(b)
dst = a.copy()
@ -67,7 +81,7 @@ def mergeDicts(a, b):
if isDict(current_src[key]) and isDict(current_dst[key]):
stack.append((current_dst[key], current_src[key]))
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])
else:
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.helpers.request import jsonified
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
log = CPLog(__name__)
class Notification(Plugin):
class Notification(Provider):
type = 'notification'
default_title = Env.get('appname')
test_message = 'ZOMG Lazors Pewpewpew!'
@ -16,11 +18,12 @@ class Notification(Plugin):
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
'core.message',
]
dont_listen_to = []
def __init__(self):
addEvent('notify.%s' % self.getName().lower(), self.notify)
addEvent('notify.%s' % self.getName().lower(), self._notify)
addApiView(self.testNotifyName(), self.test)
@ -33,10 +36,17 @@ class Notification(Plugin):
def notify(message = None, group = {}, data = None):
if not self.conf('on_snatch', default = True) and listener == 'movie.snatched':
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
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):
pass
@ -46,7 +56,7 @@ class Notification(Plugin):
log.info('Sending test to %s', test_type)
success = self.notify(
success = self._notify(
message = self.test_message,
data = {},
listener = 'test'

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

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

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

@ -1,12 +1,13 @@
from couchpotato import get_session
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.request import jsonified, getParam
from couchpotato.core.helpers.variable import tryInt, splitString
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from couchpotato.core.settings.model import Notification as Notif
from couchpotato.environment import Env
from sqlalchemy.sql.expression import or_
import threading
import time
@ -21,11 +22,6 @@ class CoreNotifier(Notification):
messages = []
listeners = []
listen_to = [
'renamer.after', 'movie.snatched',
'updater.available', 'updater.updated',
]
def __init__(self):
super(CoreNotifier, self).__init__()
@ -54,7 +50,10 @@ class CoreNotifier(Notification):
addNonBlockApiView('notification.listener', (self.addListener, self.removeListener))
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.checkMessages)
def clean(self):
@ -112,6 +111,22 @@ class CoreNotifier(Notification):
'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):
db = get_session()

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

@ -21,20 +21,17 @@ var NotificationBase = new Class({
App.addEvent('load', function(){
App.block.notification = new Block.Menu(self, {
'button_class': 'icon2.eye-open',
'class': 'notification_menu',
'onOpen': self.markAsRead.bind(self)
})
$(App.block.notification).inject(App.getBlock('search'), 'after');
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(){
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(
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})
)
, 'top');
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)
}
},
@ -64,20 +66,26 @@ var NotificationBase = new Class({
self.badge[value ? 'show' : 'hide']()
},
markAsRead: function(){
var self = this;
markAsRead: function(force_ids){
var self = this,
ids = force_ids;
var rn = self.notifications.filter(function(n){
return !n.read
})
if(!force_ids) {
var rn = self.notifications.filter(function(n){
return !n.read && n.data.important === undefined
})
var ids = []
rn.each(function(n){
ids.include(n.id)
})
var ids = []
rn.each(function(n){
ids.include(n.id)
})
}
if(ids.length > 0)
Api.request('notification.markread', {
'data': {
'ids': ids.join(',')
},
'onSuccess': function(){
self.setBadge('')
}
@ -93,11 +101,20 @@ var NotificationBase = new Class({
return;
}
Api.request('notification.listener', {
self.request = Api.request('notification.listener', {
'data': {'init':true},
'onSuccess': self.processData.bind(self)
}).send()
setInterval(function(){
if(self.request && self.request.isRunning()){
self.request.cancel();
self.startPoll()
}
}, 120000);
},
startPoll: function(){
@ -143,26 +160,41 @@ var NotificationBase = new Class({
self.startPoll()
},
showMessage: function(message){
showMessage: function(message, sticky, data){
var self = this;
if(!self.message_container)
self.message_container = new Element('div.messages').inject(document.body);
var new_message = new Element('div.message', {
'text': message
}).inject(self.message_container);
var new_message = new Element('div', {
'class': 'message' + (sticky ? ' sticky' : ''),
'html': message
}).inject(self.message_container, 'top');
setTimeout(function(){
new_message.addClass('show')
}, 10);
setTimeout(function(){
var hide_message = function(){
new_message.addClass('hide')
setTimeout(function(){
new_message.destroy();
}, 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):
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
# Extract all the settings from settings
from_address = self.conf('from')
@ -50,6 +49,5 @@ class Email(Notification):
return True
except:
log.error('E-mail failed: %s', traceback.format_exc())
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())
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
self.register()

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

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

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

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

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

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

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

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

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

@ -13,7 +13,6 @@ class Prowl(Notification):
}
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
data = {
'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):
if self.isDisabled(): return
data = {
'AuthorizationToken': self.conf('auth_token'),
'Title': self.default_title,
'Body': toUnicode(message),
'LinkTitle': toUnicode("CouchPotato"),
'link': toUnicode("https://couchpota.to/"),
'IsImportant': self.conf('important'),
'IsSilent': self.conf('silent'),
'Image': toUnicode(self.getNotificationImage('medium') + '?1'),
'Source': toUnicode(self.default_title)
}
headers = {

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

@ -1,4 +1,5 @@
from couchpotato.core.helpers.encoding import toUnicode, tryUrlencode
from couchpotato.core.helpers.variable import getTitle
from couchpotato.core.logger import CPLog
from couchpotato.core.notifications.base import Notification
from httplib import HTTPSConnection
@ -11,21 +12,26 @@ class Pushover(Notification):
app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy'
def notify(self, message = '', data = {}, listener = None):
if self.isDisabled(): return
http_handler = HTTPSConnection("api.pushover.net:443")
data = {
api_data = {
'user': self.conf('user_key'),
'token': self.app_token,
'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',
"/1/messages.json",
headers = {'Content-type': 'application/x-www-form-urlencoded'},
body = tryUrlencode(data)
body = tryUrlencode(api_data)
)
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):
if self.isDisabled(): return
data = {
'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)
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'))

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

@ -13,10 +13,8 @@ class XBMC(Notification):
listen_to = ['renamer.after']
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):
if self.isDisabled(): return
hosts = splitString(self.conf('host'))
@ -28,7 +26,7 @@ class XBMC(Notification):
if self.use_json_notifications.get(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', {}),
])
else:
@ -90,7 +88,7 @@ class XBMC(Notification):
self.use_json_notifications[host] = True
# 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:
if (result.get('result') and result['result'] == 'OK'):
log.debug('Message delivered successfully!')
@ -113,7 +111,7 @@ class XBMC(Notification):
server = 'http://%s/xbmcCmds/' % host
# 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
# 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',
'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):
fireEvent('schedule.interval', 'automation.add_movies', self.addMovies, hours = self.conf('hour', default = 12))
addEvent('app.load', self.setCrons)
if not Env.get('dev'):
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):
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.settings.model import FileType, File
from couchpotato.environment import Env
from flask.helpers import send_file
from werkzeug.exceptions import NotFound
import os.path
import time
import traceback
@ -71,7 +73,7 @@ class FileManager(Plugin):
db = get_session()
for root, dirs, walk_files in os.walk(Env.get('cache_dir')):
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)
f = db.query(File).filter(File.path == toUnicode(file_path)).first()
if not f:
@ -81,11 +83,13 @@ class FileManager(Plugin):
def showCacheFile(self, filename = ''):
cache_dir = Env.get('cache_dir')
filename = os.path.basename(filename)
file_path = os.path.join(Env.get('cache_dir'), os.path.basename(filename))
from flask.helpers import send_from_directory
return send_from_directory(cache_dir, filename)
if not os.path.isfile(file_path):
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 = {}):

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

@ -1,17 +1,18 @@
.page.log .nav {
display: block;
text-align: center;
padding: 20px 0;
padding: 0 0 30px;
margin: 0;
font-size: 20px;
position: fixed;
width: 960px;
width: 100%;
bottom: 0;
left: 0;
background: #4E5969;
}
.page.log .nav li {
display: inline;
display: inline-block;
padding: 5px 10px;
margin: 0;
cursor: pointer;
@ -24,7 +25,17 @@
.page.log .nav li.active {
font-weight: bold;
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 {

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

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

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

@ -185,6 +185,8 @@ class Manage(Plugin):
# Add it to release and update the info
fireEvent('release.add', group = group)
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

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

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

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

@ -1,13 +1,15 @@
var MovieList = new Class({
Implements: [Options],
Implements: [Events, Options],
options: {
navigation: true,
limit: 50,
load_more: true,
loader: true,
menu: [],
add_new: false
add_new: false,
force_view: false
},
movies: [],
@ -42,8 +44,11 @@ var MovieList = new Class({
}) : null
);
self.changeView(self.getSavedView() || self.options.view || 'details');
if($(window).getSize().x <= 480 && !self.options.force_view)
self.changeView('list');
else
self.changeView(self.getSavedView() || self.options.view || 'details');
self.getMovies();
App.addEvent('movie.added', self.movieAdded.bind(self))
@ -120,7 +125,7 @@ var MovieList = new Class({
if(!self.navigation_counter) return;
self.navigation_counter.set('text', (count || 0));
self.navigation_counter.set('text', (count || 0) + ' movies');
},
@ -144,65 +149,67 @@ var MovieList = new Class({
var self = this;
var chars = '#ABCDEFGHIJKLMNOPQRSTUVWXYZ';
self.current_view = self.getSavedView() || 'details';
self.el.addClass(self.current_view+'_list')
self.el.addClass('with_navigation')
self.navigation = new Element('div.alph_nav').adopt(
self.navigation_actions = new Element('ul.inlay.actions.reversed'),
self.navigation_counter = new Element('span.counter[title=Total]'),
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li)': function(e, el){
self.movie_list.empty()
self.activateLetter(el.get('data-letter'))
self.getMovies()
}
}
}),
self.navigation_search_input = new Element('input.inlay', {
'placeholder': 'Search',
'events': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
}),
self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
self.navigation = new Element('div.alph_nav').grab(
new Element('div').adopt(
self.navigation_alpha = new Element('ul.numbers', {
'events': {
'click:relay(li)': function(e, el){
self.movie_list.empty()
self.activateLetter(el.get('data-letter'))
self.getMovies()
}
})
}
}),
self.navigation_counter = new Element('span.counter[title=Total]'),
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': {
'keyup': self.search.bind(self),
'change': self.search.bind(self)
}
}),
self.navigation_menu = new Block.Menu(self),
self.mass_edit_form = new Element('div.mass_edit_form').adopt(
new Element('span.select').adopt(
self.mass_edit_select = new Element('input[type=checkbox].inlay', {
'events': {
'change': self.massEditToggleAll.bind(self)
}
}),
self.mass_edit_selected = new Element('span.count', {'text': 0}),
self.mass_edit_selected_label = new Element('span', {'text': 'selected'})
),
new Element('div.quality').adopt(
self.mass_edit_quality = new Element('select'),
new Element('a.button.orange', {
'text': 'Change quality',
'events': {
'click': self.changeQualitySelected.bind(self)
}
})
),
new Element('div.delete').adopt(
new Element('span[text=or]'),
new Element('a.button.red', {
'text': 'Delete',
'events': {
'click': self.deleteSelected.bind(self)
}
})
),
new Element('div.refresh').adopt(
new Element('span[text=or]'),
new Element('a.button.green', {
'text': 'Refresh',
'events': {
'click': self.refreshSelected.bind(self)
}
})
)
)
)
).inject(self.el, 'top');
@ -247,18 +254,19 @@ var MovieList = new Class({
});
// Get available chars and highlight
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
'onComplete': function(json){
json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
if(self.navigation.isDisplayed() || self.navigation.isVisible())
Api.request('movie.available_chars', {
'data': Object.merge({
'status': self.options.status
}, self.filter),
'onSuccess': function(json){
json.chars.split('').each(function(c){
self.letters[c.capitalize()].addClass('available')
})
}
});
// Add menu or hide
if (self.options.menu.length > 0)
@ -266,17 +274,7 @@ var MovieList = new Class({
self.navigation_menu.addLink(menu_item);
})
else
self.navigation_menu.hide()
self.nav_scrollspy = new ScrollSpy({
min: 10,
onEnter: function(){
self.navigation.addClass('float')
},
onLeave: function(){
self.navigation.removeClass('float')
}
});
self.navigation_menu.hide();
},
@ -475,12 +473,39 @@ var MovieList = new Class({
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', {
'data': Object.merge({
'status': self.options.status,
'limit_offset': self.options.limit + ',' + self.offset
}, 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.addMovies(json.movies, json.total);
if(self.scrollspy) {
@ -488,7 +513,8 @@ var MovieList = new Class({
self.scrollspy.start();
}
self.checkIfEmpty()
self.checkIfEmpty();
self.fireEvent('loaded');
}
});
},
@ -515,10 +541,10 @@ var MovieList = new Class({
self.title[is_empty ? 'hide' : 'show']()
if(self.description)
self.description[is_empty ? 'hide' : 'show']()
self.description.setStyle('display', [is_empty ? 'none' : ''])
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)
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()
}
else {
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;
}
});
}
else
self.showHelper();
},
show: function(e){
createReleases: function(){
var self = this;
if(e)
(e).preventDefault();
if(!self.options_container){
self.options_container = new Element('div.options').adopt(
self.release_container = new Element('div.releases.table').adopt(
self.trynext_container = new Element('div.buttons.try_container')
)
).inject(self.movie, 'top');
);
// Header
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);
},
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){
return release.info[type] || 'n/a'
},
@ -251,14 +294,15 @@ MA.Release = new Class({
var release_el = self.release_container.getElement('#release_'+release.id),
icon = release_el.getElement('.download.icon');
icon.addClass('spinner');
self.movie.busy(true);
Api.request('release.download', {
'data': {
'id': release.id
},
'onComplete': function(json){
icon.removeClass('spinner')
self.movie.busy(false);
if(json.success)
icon.addClass('completed');
else
@ -281,6 +325,8 @@ MA.Release = new Class({
tryNextRelease: function(movie_id){
var self = this;
self.createReleases();
if(self.last_release)
self.ignore(self.last_release);

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

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

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

@ -22,7 +22,10 @@ var Movie = new Class({
addEvents: function(){
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){
App.addEvent(listener+'.'+self.data.id, function(notification){
@ -57,17 +60,19 @@ var Movie = new Class({
var self = this;
if(!set_busy){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, 400);
}
setTimeout(function(){
if(self.spinner){
self.mask.fade('out');
setTimeout(function(){
if(self.mask)
self.mask.destroy();
if(self.spinner)
self.spinner.el.destroy();
self.spinner = null;
self.mask = null;
}, 400);
}
}, 1000)
}
else if(!self.spinner) {
self.createMask();
@ -126,12 +131,14 @@ var Movie = new Class({
self.thumbnail = File.Select.single('poster', self.data.library.files),
self.data_container = new Element('div.data.inlay.light').adopt(
self.info_container = new Element('div.info').adopt(
self.title = new Element('div.title', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
}),
new Element('div.title').adopt(
self.title = new Element('span', {
'text': self.getTitle() || 'n/a'
}),
self.year = new Element('div.year', {
'text': self.data.library.year || 'n/a'
})
),
self.rating = new Element('div.rating.icon', {
'text': self.data.library.rating
}),

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

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

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

@ -7,33 +7,30 @@ Block.Search = new Class({
create: function(){
var self = this;
var focus_timer = 0;
self.el = new Element('div.search_form').adopt(
new Element('div.input').adopt(
self.input = new Element('input.inlay', {
self.input = new Element('input', {
'placeholder': 'Search & add a new movie',
'events': {
'keyup': self.keyup.bind(self),
'focus': function(){
if(focus_timer) clearTimeout(focus_timer);
self.el.addClass('focused')
if(this.get('value'))
self.hideResults(false)
},
'blur': function(){
(function(){
focus_timer = (function(){
self.el.removeClass('focused')
}).delay(2000);
}).delay(100);
}
}
}),
new Element('span.enter', {
new Element('a.icon2', {
'events': {
'click': self.keyup.bind(self)
},
'text':'Enter'
}),
new Element('a', {
'events': {
'click': self.clear.bind(self)
'click': self.clear.bind(self),
'touchend': self.clear.bind(self)
}
})
),
@ -59,13 +56,21 @@ Block.Search = new Class({
var self = this;
(e).preventDefault();
self.last_q = '';
self.input.set('value', '');
self.input.focus()
if(self.last_q === ''){
self.input.blur()
self.last_q = null;
}
else {
self.last_q = '';
self.input.set('value', '');
self.input.focus()
self.movies = []
self.results.empty()
self.el.removeClass('filled')
self.movies = []
self.results.empty()
self.el.removeClass('filled')
}
},
hideResults: function(bool){
@ -92,8 +97,10 @@ Block.Search = new Class({
self.el[self.q() ? 'addClass' : 'removeClass']('filled')
if(self.q() != self.last_q && (['enter'].indexOf(e.key) > -1 || e.type == 'click'))
self.autocomplete()
if(self.q() != self.last_q){
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', {
'id': info.imdb
}).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.data_container = new Element('div.data', {
'tween': {
@ -207,11 +219,6 @@ Block.Search.Item = new Class({
'click': self.showOptions.bind(self)
}
}).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(
self.title = new Element('h2', {
'text': info.titles[0]
@ -219,28 +226,11 @@ Block.Search.Item = new Class({
self.year = info.year ? new Element('span.year', {
'text': info.year
}) : 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){
self.alternativeTitle({
@ -319,12 +309,9 @@ Block.Search.Item = new Class({
}
self.options_el.grab(
new Element('div').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', {
'class': self.info.in_wanted && self.info.in_wanted.profile || in_library ? 'in_library_wanted' : ''
}).adopt(
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
}) : (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.logger import CPLog
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__)
@ -30,6 +30,21 @@ class ProfilePlugin(Plugin):
})
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):
@ -129,6 +144,9 @@ class ProfilePlugin(Plugin):
db.delete(p)
db.commit()
# Force defaults on all empty profile movies
self.forceDefaults()
success = True
except Exception, 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);
}
.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 {
height: 20px;
width: 20px;
position: absolute;
margin-left: 690px;
padding: 14px;
padding: 25px 20px;
background-position: center;
right: 0;
cursor: pointer;
opacity: 0.6;
}
.profile > .delete:hover {
opacity: 1;
}
.profile .ctrlHolder:hover {
background: none;
}
.profile .qualities {
@ -34,7 +44,8 @@
.profile .wait_for {
position: absolute;
margin: -45px 0 0 437px;
right: 60px;
top: 0;
}
.profile .wait_for input {
@ -61,6 +72,10 @@
margin-right: 10px;
}
.profile .type .check {
margin-top: -1px;
}
.profile .quality_type select {
width: 186px;
margin-left: -1px;
@ -71,13 +86,13 @@
}
.profile .types .type .handle {
background: url('./handle.png') center;
background: url('../../static/profile_plugin/handle.png') center;
display: inline-block;
height: 20px;
width: 20px;
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
margin: 0;
}
@ -105,9 +120,9 @@
}
#profile_ordering li {
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
border-bottom: 1px solid rgba(255,255,255,0.2);
padding: 0 5px;
}
@ -126,7 +141,7 @@
}
#profile_ordering li .handle {
background: url('./handle.png') center;
background: url('../../static/profile_plugin/handle.png') center;
width: 20px;
float: right;
}

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

@ -19,12 +19,12 @@ class QualityPlugin(Plugin):
qualities = [
{'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]},
{'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, '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': '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': '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': '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': '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']},

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

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

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

@ -13,7 +13,7 @@ rename_options = {
'thename': 'The Moviename',
'year': 'Year (2011)',
'first': 'First letter (M)',
'quality': 'Quality (720P)',
'quality': 'Quality (720p)',
'video': 'Video (x264)',
'audio': 'Audio (DTS)',
'group': 'Releasegroup name',
@ -113,6 +113,15 @@ config = [{
'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,
'name': 'ntfs_permission',
'label': 'NTFS Permission',

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

@ -2,12 +2,13 @@ from couchpotato import get_session
from couchpotato.api import addApiView
from couchpotato.core.event import addEvent, fireEvent, fireEventAsync
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, \
getImdb
getImdb, link, symlink
from couchpotato.core.logger import CPLog
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
import errno
import os
@ -18,7 +19,6 @@ import traceback
log = CPLog(__name__)
class Renamer(Plugin):
renaming_started = False
@ -27,7 +27,12 @@ class Renamer(Plugin):
def __init__(self):
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)
@ -35,22 +40,42 @@ class Renamer(Plugin):
addEvent('app.load', self.scan)
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):
if self.conf('run_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched', self.checkSnatched, minutes = self.conf('run_every'))
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('force_every') > 0:
fireEvent('schedule.interval', 'renamer.check_snatched_forced', self.scan, hours = self.conf('force_every'))
fireEvent('schedule.remove', 'renamer.check_snatched_forced')
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)
return True
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({
'success': True
})
def scan(self):
def scan(self, movie_folder = None, download_info = None):
if self.isDisabled():
return
@ -60,17 +85,42 @@ class Renamer(Plugin):
return
# 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')):
log.debug('"To" and "From" have to exist.')
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')):
l = log.debug if movie_folder else log.error
l('Both the "To" and "From" have to exist.')
return
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.')
return
groups = fireEvent('scanner.scan', folder = self.conf('from'), single = True)
elif (movie_folder and movie_folder in [self.conf('to'), self.conf('from')]):
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
# 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')
folder_name = self.conf('folder_name')
file_name = self.conf('file_name')
@ -79,12 +129,8 @@ class Renamer(Plugin):
separator = self.conf('separator')
# Statusses
done_status = fireEvent('status.get', 'done', single = True)
active_status = fireEvent('status.get', 'active', single = True)
downloaded_status = fireEvent('status.get', 'downloaded', single = True)
snatched_status = fireEvent('status.get', 'snatched', single = True)
db = get_session()
done_status, active_status, downloaded_status, snatched_status = \
fireEvent('status.get', ['done', 'active', 'downloaded', 'snatched'], single = True)
for group_identifier in groups:
@ -304,7 +350,7 @@ class Renamer(Plugin):
else:
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')
# Notify on rename fail
@ -325,7 +371,8 @@ class Renamer(Plugin):
db.commit()
# 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')
for current_file in group['files']['leftover']:
remove_files.append(current_file)
@ -350,7 +397,7 @@ class Renamer(Plugin):
os.remove(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)
except:
@ -375,12 +422,15 @@ class Renamer(Plugin):
self.makeDir(os.path.dirname(dst))
try:
self.moveFile(src, dst)
self.moveFile(src, dst, forcemove = not self.downloadIsTorrent(download_info))
group['renamed_files'].append(dst)
except:
log.error('Failed moving the file "%s" : %s', (os.path.basename(src), traceback.format_exc()))
self.tagDir(group, 'failed_rename')
if self.conf('file_action') != 'move' and self.downloadIsTorrent(download_info):
self.tagDir(group, 'renamed already')
# Remove matching releases
for release in remove_releases:
log.debug('Removing release %s', release.identifier)
@ -426,36 +476,40 @@ class Renamer(Plugin):
return rename_files
# This adds a file to ignore / tag a release so it is ignored later
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']:
rename_files[group['parentdir']] = group['parentdir'].replace(group['dirname'], '_%s_%s' % (tag.upper(), group['dirname']))
else: # Add it to filename
for file_type in group['files']:
for rename_me in group['files'][file_type]:
filename = os.path.basename(rename_me)
rename_files[rename_me] = rename_me.replace(filename, '_%s_%s' % (tag.upper(), filename))
text = """This file is from CouchPotato
It has marked this release as "%s"
This file hides the release from the renamer
Remove it if you want it to be renamed (again, or at least let it try again)
""" % tag
for src in rename_files:
if rename_files[src]:
dst = rename_files[src]
log.info('Renaming "%s" to "%s"', (src, dst))
if ignore_file:
self.createFile(ignore_file, text)
# 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)
try:
shutil.move(old, dest)
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)
try:
os.chmod(dest, Env.getPermission('file'))
@ -524,16 +578,14 @@ class Renamer(Plugin):
loge('Couldn\'t remove empty directory %s: %s', (folder, traceback.format_exc()))
def checkSnatched(self):
if self.checking_snatched:
log.debug('Already checking snatched')
self.checking_snatched = True
snatched_status = fireEvent('status.get', 'snatched', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
failed_status = fireEvent('status.get', 'failed', single = True)
done_status = fireEvent('status.get', 'done', single = True)
snatched_status, ignored_status, failed_status, done_status = \
fireEvent('status.get', ['snatched', 'ignored', 'failed', 'done'], single = True)
db = get_session()
rels = db.query(Release).filter_by(status_id = snatched_status.get('id')).all()
@ -571,8 +623,16 @@ class Renamer(Plugin):
found = False
for item in statuses:
if item['name'] == nzbname or rel_dict['info']['name'] in item['name'] or getImdb(item['name']) == movie_dict['library']['identifier']:
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']:
found_release = True
if found_release:
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))
@ -580,16 +640,18 @@ class Renamer(Plugin):
pass
elif item['status'] == 'failed':
fireEvent('download.remove_failed', item, single = True)
rel.status_id = failed_status.get('id')
rel.last_edit = int(time.time())
db.commit()
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.last_edit = int(time.time())
db.commit()
elif item['status'] == 'completed':
log.info('Download of %s completed!', item['name'])
scan_required = True
if item['id'] and item['downloader'] and item['folder']:
fireEventAsync('renamer.scan', movie_folder = item['folder'], download_info = item)
else:
scan_required = True
found = True
break
@ -606,3 +668,38 @@ class Renamer(Plugin):
self.checking_snatched = False
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']

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

@ -11,6 +11,7 @@ from subliminal.videos import Video
import enzyme
import os
import re
import threading
import time
import traceback
@ -74,7 +75,7 @@ class Scanner(Plugin):
'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 = [
'[ _\.-]+cd[ _\.-]*([0-9a-d]+)', #*cd1
'[ _\.-]+dvd[ _\.-]*([0-9a-d]+)', #*dvd1
@ -100,7 +101,7 @@ class Scanner(Plugin):
addEvent('scanner.name_year', self.getReleaseNameYear)
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))
@ -118,8 +119,7 @@ class Scanner(Plugin):
try:
files = []
for root, dirs, walk_files in os.walk(folder):
for filename in walk_files:
files.append(os.path.join(root, filename))
files.extend(os.path.join(root, filename) for filename in walk_files)
except:
log.error('Failed getting files from %s: %s', (folder, traceback.format_exc()))
else:
@ -177,17 +177,25 @@ class Scanner(Plugin):
# Group files minus extension
ignored_identifiers = []
for identifier, group in movie_files.iteritems():
if identifier not in group['identifiers'] and len(identifier) > 0: group['identifiers'].append(identifier)
log.debug('Grouping files: %s', identifier)
has_ignored = 0
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])
group['unsorted_files'].extend(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
if self.shuttingDown():
break
@ -313,6 +321,11 @@ class Scanner(Plugin):
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
processed_movies = {}
total_found = len(valid_files)
@ -322,15 +335,17 @@ class Scanner(Plugin):
except:
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
# images = self.getImages(group['unsorted_files'])
group['files'] = {
'movie_extra': self.getMovieExtras(group['unsorted_files']),
'subtitle': self.getSubtitles(group['unsorted_files']),
'subtitle_extra': self.getSubtitlesExtras(group['unsorted_files']),
'nfo': self.getNfo(group['unsorted_files']),
'trailer': self.getTrailers(group['unsorted_files']),
#'backdrop': images['backdrop'],
'leftover': set(group['unsorted_files']),
}
@ -345,7 +360,7 @@ class Scanner(Plugin):
continue
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
group['subtitle_language'] = self.getSubtitleLanguage(group) if not simple else {}
@ -375,7 +390,7 @@ class Scanner(Plugin):
del group['unsorted_files']
# Determine movie
group['library'] = self.determineMovie(group)
group['library'] = self.determineMovie(group, download_info = download_info)
if not group['library']:
log.error('Unable to determine movie: %s', group['identifiers'])
else:
@ -388,6 +403,11 @@ class Scanner(Plugin):
if on_found:
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:
log.info('Found %s movies in the folder %s', (len(processed_movies), folder))
else:
@ -395,7 +415,7 @@ class Scanner(Plugin):
return processed_movies
def getMetaData(self, group, folder = ''):
def getMetaData(self, group, folder = '', download_info = None):
data = {}
files = list(group['files']['movie'])
@ -417,9 +437,13 @@ class Scanner(Plugin):
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)
if not data['quality']:
data['quality'] = fireEvent('quality.single', 'dvdr' if group['is_dvd'] else 'dvdrip', single = True)
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_type'] = 'HD' if data.get('resolution_width', 0) >= 1280 or data['quality'].get('hd') else 'SD'
@ -495,17 +519,22 @@ class Scanner(Plugin):
return detected_languages
def determineMovie(self, group):
imdb_id = None
def determineMovie(self, group, download_info = 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']
# Check for CP(imdb_id) string in the file paths
for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file)
if imdb_id:
log.debug('Found movie via CP tag: %s', cur_file)
break
if not imdb_id:
for cur_file in files['movie']:
imdb_id = self.getCPImdb(cur_file)
if imdb_id:
log.debug('Found movie via CP tag: %s', cur_file)
break
# Check and see if nfo contains the imdb-id
if not imdb_id:

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

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

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

@ -25,12 +25,13 @@ config = [{
'label': 'Required words',
'default': '',
'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',
'label': 'Ignored words',
'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',
@ -40,6 +41,14 @@ config = [{
'type': 'dropdown',
'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',

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'))
def allMoviesView(self):
@ -141,8 +146,7 @@ class Searcher(Plugin):
pre_releases = fireEvent('quality.pre_releases', single = True)
release_dates = fireEvent('library.update_release_date', identifier = movie['library']['identifier'], merge = True)
available_status = fireEvent('status.get', 'available', single = True)
ignored_status = fireEvent('status.get', 'ignored', single = True)
available_status, ignored_status = fireEvent('status.get', ['available', 'ignored'], single = True)
found_releases = []
@ -157,7 +161,7 @@ class Searcher(Plugin):
ret = False
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))
continue
@ -285,10 +289,10 @@ class Searcher(Plugin):
if filedata == 'try_next':
return filedata
successful = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
if successful:
download_result = fireEvent('download', data = data, movie = movie, manual = manual, filedata = filedata, single = True)
log.debug('Downloader result: %s', download_result)
if download_result:
try:
# Mark release as snatched
db = get_session()
@ -298,6 +302,15 @@ class Searcher(Plugin):
done_status = fireEvent('status.get', 'done', single = True)
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()
log_movie = '%s (%s) in %s' % (getTitle(movie['library']), movie['library']['year'], rls.quality.label)
@ -333,7 +346,7 @@ class Searcher(Plugin):
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
@ -357,7 +370,7 @@ class Searcher(Plugin):
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)
retention = Env.setting('retention', section = 'nzb')
@ -370,8 +383,9 @@ class Searcher(Plugin):
movie_words = re.split('\W+', simplifyString(movie_name))
nzb_name = simplifyString(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
for req_set in required_words:
req = splitString(req_set, '&')
@ -381,19 +395,24 @@ class Searcher(Plugin):
log.info2("Wrong: Required word missing: %s" % nzb['name'])
return False
# Ignore releases
ignored_words = splitString(self.conf('ignored_words').lower())
blacklisted = list(set(nzb_words) & set(ignored_words) - set(movie_words))
if self.conf('ignored_words') and blacklisted:
log.info2("Wrong: '%s' blacklisted words: %s" % (nzb['name'], ", ".join(blacklisted)))
ignored_match = 0
for ignored_set in ignored_words:
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
# Ignore porn stuff
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))
if pron_words:
log.info('Wrong: %s, probably pr0n', (nzb['name']))
return False
#qualities = fireEvent('quality.all', single = True)
preferred_quality = fireEvent('quality.single', identifier = quality['identifier'], single = True)
# Contains lower quality string

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

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

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

@ -20,7 +20,7 @@ config = [{
},
{
'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',

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

@ -22,14 +22,14 @@ config = [{
'name': 'quality',
'default': '720p',
'type': 'dropdown',
'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')],
'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')],
},
{
'name': 'name',
'label': 'Naming',
'default': '<filename>-trailer',
'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
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)
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:
log.debug('Trailer already exists: %s', destination)

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

@ -5,6 +5,7 @@
bottom: 0;
left: 0;
right: 0;
padding: 0;
}
.page.userscript .frame.loading {
@ -12,3 +13,26 @@
font-size: 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.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;
self.settings.createGroup({
'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'
}).inject(self.settings.tabs.automation.content, 'top').adopt(
(userscript ? [new Element('a.userscript.button', {
new Element('a.userscript.button', {
'text': 'Install userscript',
'href': Api.createUrl('userscript.get')+randomString()+'/couchpotato.user.js',
'target': '_self'
}), new Element('span.or[text=or]')] : null),
'target': '_blank'
}),
new Element('span.or[text=or]'),
new Element('span.bookmarklet').adopt(
new Element('a.button.green', {
'text': '+CouchPotato',

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

@ -1,4 +1,9 @@
// ==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
// @description Add movies like a real CouchPotato
// @grant none

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

@ -1,49 +1,60 @@
.page.wizard .uniForm {
width: 80%;
margin: 0 auto 30px;
margin: 0 0 30px;
width: 83%;
}
.page.wizard h1 {
padding: 10px 30px;
margin: 0;
padding: 10px 0;
margin: 0 5px;
display: block;
font-size: 30px;
margin-top: 80px;
}
.page.wizard .description {
padding: 10px 30px;
font-size: 18px;
padding: 10px 5px;
font-size: 1.45em;
line-height: 1.4em;
display: block;
}
.page.wizard .tab_wrapper {
background: #5c697b;
padding: 10px 0;
font-size: 18px;
height: 65px;
font-size: 1.75em;
position: fixed;
top: 0;
margin: 0;
width: 100%;
min-width: 960px;
left: 0;
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 {
text-align: center;
padding: 0;
margin: 0;
margin: 0 auto;
display: block;
height: 100%;
width: 100%;
max-width: 960px;
}
.page.wizard .tabs li {
display: inline-block;
height: 100%;
}
.page.wizard .tabs li a {
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 {
border-right: 10px solid transparent;
@ -61,27 +72,13 @@
.page.wizard form > div {
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 {
padding: 20px;
font-size: 25px;
margin: 10px 30px 80px;
display: block;
text-align: center;
}
.page.wizard .button.green {
padding: 20px;
font-size: 25px;
margin: 10px 0 80px;
display: block;
}
.page.wizard .tab_nzb_providers {
margin: 20px 0 0 0;

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

@ -9,27 +9,12 @@ Page.Wizard = new Class({
headers: {
'welcome': {
'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', {
'styles': {
'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': {
'title': 'General',
@ -178,7 +163,7 @@ Page.Wizard = new Class({
'href': App.createUrl('wizard/'+group),
'text': (self.headers[group].label || group).capitalize()
})
).inject(tabs);
).inject(tabs)
}
else
@ -214,13 +199,7 @@ Page.Wizard = new Class({
self.el.getElement('.t_searcher').hide();
// Add pointer
new Element('.tab_wrapper').wraps(tabs).adopt(
self.pointer = new Element('.pointer', {
'tween': {
'transition': 'quint:in:out'
}
})
);
new Element('.tab_wrapper').wraps(tabs);
// Add nav
var minimum = self.el.getSize().y-window.getSize().y;
@ -232,16 +211,18 @@ Page.Wizard = new Class({
if(!t) return;
var func = function(){
var ct = t.getCoordinates();
self.pointer.tween('left', ct.left+(ct.width/2)-(self.pointer.getWidth()/2));
// Activate all previous ones
self.groups.each(function(groups2, nr2){
var t2 = self.el.getElement('.t_'+groups2);
t2[nr2 > nr ? 'removeClass' : 'addClass' ]('done');
})
g.tween('opacity', 1);
}
if(nr == 0)
func();
var ss = new ScrollSpy( {
new ScrollSpy( {
min: function(){
var c = g.getCoordinates();
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.providers.base import Provider
from couchpotato.environment import Env
from couchpotato.core.helpers.variable import splitString
import time
log = CPLog(__name__)
@ -59,7 +60,26 @@ class Automation(Provider):
type_value = movie.get(minimal_type, 0)
type_min = self.getMinimal(minimal_type)
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 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:
cookiejar = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
opener.addheaders = []
urllib2.install_opener(opener)
log.info2('Logging into %s', self.urls['login'])
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:
# Statuses
active_status = fireEvent('status.get', 'active', single = True)
done_status = fireEvent('status.get', 'done', single = True)
active_status, done_status = fireEvent('status.get', ['active', 'done'], single = True)
for movie in l.movies:
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.helpers.encoding import tryUrlencode
from couchpotato.core.helpers.request import jsonified, getParams
from couchpotato.core.helpers.variable import tryInt
from couchpotato.core.logger import CPLog
from couchpotato.core.providers.movie.base import MovieProvider
from couchpotato.core.settings.model import Movie
from couchpotato.environment import Env
import time
log = CPLog(__name__)
@ -18,19 +20,37 @@ class CouchPotatoApi(MovieProvider):
'is_movie': 'https://couchpota.to/api/ismovie/%s/',
'eta': 'https://couchpota.to/api/eta/%s/',
'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
api_version = 1
def __init__(self):
#addApiView('movie.suggest', self.suggestView)
addEvent('movie.info', self.getInfo, priority = 1)
addEvent('movie.search', self.search, priority = 1)
addEvent('movie.release_date', self.getReleaseDate)
addEvent('movie.suggest', self.suggest)
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):
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-API': self.api_version,
'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',
'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': '',
'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',
'name': 'newznab',
'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://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,
'options': [
{
@ -23,20 +24,27 @@ config = [{
},
{
'name': 'use',
'default': '0,0,0,0'
'default': '0,0,0,0,0,0'
},
{
'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',
},
{
'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',
'default': ',,,',
'default': ',,,,,',
'label': 'Api Key',
'description': 'Can be found on your profile page',
'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.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.providers.base import ResultList
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),
'detail_url': '%sdetails/%s' % (cleanHost(host['host']), tryUrlencode(nzb_id)),
'content': self.getTextElement(nzb, 'description'),
'score': host['extra_score'],
})
def getHosts(self):
@ -83,13 +84,15 @@ class Newznab(NZBProvider, RSS):
uses = splitString(str(self.conf('use')))
hosts = splitString(self.conf('host'))
api_keys = splitString(self.conf('api_key'))
extra_score = splitString(self.conf('extra_score'))
list = []
for nr in range(len(hosts)):
list.append({
'use': uses[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

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

@ -18,6 +18,14 @@ config = [{
'name': 'enabled',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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',
'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': '',
'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': '',
'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,
'label': 'Proxy server',
'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': '',
'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': '',
'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',
'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.save()
# After save (for re-interval etc)
fireEvent('setting.save.%s.%s.after' % (section, option), single = True)
return jsonified({
'success': True,
})

26
couchpotato/core/settings/model.py

@ -76,7 +76,7 @@ class LibraryTitle(Entity):
title = Field(Unicode)
simple_title = Field(Unicode, index = True)
default = Field(Boolean, index = True)
default = Field(Boolean, default = False, index = True)
language = OneToMany('Language')
libraries = ManyToOne('Library')
@ -141,12 +141,12 @@ class Status(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')
identifier = Field(String(20), unique = True)
label = Field(Unicode(20))
order = Field(Integer, index = True)
order = Field(Integer, default = 0, index = True)
size_min = Field(Integer)
size_max = Field(Integer)
@ -160,21 +160,27 @@ class Profile(Entity):
using_options(order_by = 'order')
label = Field(Unicode(50))
order = Field(Integer, index = True)
core = Field(Boolean)
hide = Field(Boolean)
order = Field(Integer, default = 0, index = True)
core = Field(Boolean, default = False)
hide = Field(Boolean, default = False)
movie = OneToMany('Movie')
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):
""""""
using_options(order_by = 'order')
order = Field(Integer, index = True)
finish = Field(Boolean)
wait_for = Field(Integer)
order = Field(Integer, default = 0, index = True)
finish = Field(Boolean, default = True)
wait_for = Field(Integer, default = 0)
quality = ManyToOne('Quality')
profile = ManyToOne('Profile')
@ -185,7 +191,7 @@ class File(Entity):
path = Field(Unicode(255), nullable = False, unique = True)
part = Field(Integer, default = 1)
available = Field(Boolean)
available = Field(Boolean, default = True)
type = ManyToOne('FileType')
properties = OneToMany('FileProperty')

12
couchpotato/runner.py

@ -190,9 +190,12 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
version_control(db, repo, version = latest_db_version)
current_db_version = db_version(db, repo)
if current_db_version < latest_db_version and not development:
log.info('Doing database upgrade. From %d to %d', (current_db_version, latest_db_version))
upgrade(db, repo)
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))
upgrade(db, repo)
# Configure Database
from couchpotato.core.settings.model import setup
@ -238,7 +241,8 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
from tornado.ioloop import IOLoop
web_container = WSGIContainer(app)
web_container._log = _log
loop = IOLoop.instance()
loop = IOLoop.current()
application = Application([
(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