diff --git a/couchpotato/core/_base/_core/__init__.py b/couchpotato/core/_base/_core/__init__.py index c8c3fda6..4d1a6840 100644 --- a/couchpotato/core/_base/_core/__init__.py +++ b/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', diff --git a/couchpotato/core/downloaders/base.py b/couchpotato/core/downloaders/base.py index 70500dc0..69449249 100644 --- a/couchpotato/core/downloaders/base.py +++ b/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,34 @@ 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(), + } + + return mergeDicts(defaults, result) + diff --git a/couchpotato/core/downloaders/nzbget/__init__.py b/couchpotato/core/downloaders/nzbget/__init__.py index 403a7e7d..5349914a 100644 --- a/couchpotato/core/downloaders/nzbget/__init__.py +++ b/couchpotato/core/downloaders/nzbget/__init__.py @@ -48,6 +48,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.', + }, ], } ], diff --git a/couchpotato/core/downloaders/nzbget/main.py b/couchpotato/core/downloaders/nzbget/main.py index 82d8a363..1bc54fd5 100644 --- a/couchpotato/core/downloaders/nzbget/main.py +++ b/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.logger import CPLog +from datetime import timedelta import re +import shutil import socket import traceback import xmlrpclib @@ -50,7 +52,107 @@ class NZBGet(Downloader): if xml_response: log.info('NZB sent successfully to NZBGet') - return True + groups = rpc.listgroups() + nzb_id = [item['NZBID'] for item in groups if item['NZBFilename'] == nzb_name][0] + 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'), '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']) + statuses.append({ + 'id': item['NZBID'], + '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: + 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'])) + statuses.append({ + 'id': item['NZBID'], + '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)), + }) + + 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() + if rpc.editqueue('HistoryDelete', 0, "", [tryInt(item['id'])]): + path = [hist['DestDir'] for hist in history if hist['NZBID'] == item['id']][0] + shutil.rmtree(path, True) + except: + log.error('Failed deleting: %s', traceback.format_exc(0)) + return False + + return True diff --git a/couchpotato/core/downloaders/nzbvortex/main.py b/couchpotato/core/downloaders/nzbvortex/main.py index 1462c678..e6cbd027 100644 --- a/couchpotato/core/downloaders/nzbvortex/main.py +++ b/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 diff --git a/couchpotato/core/downloaders/sabnzbd/main.py b/couchpotato/core/downloaders/sabnzbd/main.py index a287f119..8b374698 100644 --- a/couchpotato/core/downloaders/sabnzbd/main.py +++ b/couchpotato/core/downloaders/sabnzbd/main.py @@ -1,4 +1,4 @@ -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 @@ -17,8 +17,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 +30,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 +46,19 @@ 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.') + if sab_data.get('error'): + log.error('Error getting data from SABNZBd: %s', sab_data.get('error')) 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'): 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(sab_data) return False def getAllDownloadStatus(self): @@ -85,14 +84,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, }) @@ -133,21 +131,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 diff --git a/couchpotato/core/downloaders/transmission/main.py b/couchpotato/core/downloaders/transmission/main.py index 5c13af6e..6c4607fb 100644 --- a/couchpotato/core/downloaders/transmission/main.py +++ b/couchpotato/core/downloaders/transmission/main.py @@ -62,7 +62,7 @@ class Transmission(Downloader): if torrent_params: trpc.set_torrent(remote_torrent['torrent-added']['hashString'], torrent_params) - return True + return self.downloadReturnId(remote_torrent['torrent-added']['hashString']) except Exception, err: log.error('Failed to change settings for transfer: %s', err) return False diff --git a/couchpotato/core/downloaders/utorrent/main.py b/couchpotato/core/downloaders/utorrent/main.py index 5953b117..c64db135 100644 --- a/couchpotato/core/downloaders/utorrent/main.py +++ b/couchpotato/core/downloaders/utorrent/main.py @@ -1,6 +1,6 @@ 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 @@ -66,7 +66,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 +103,7 @@ class uTorrent(Downloader): log.debug('Nothing in queue') return False - statuses = [] + statuses = StatusList(self) # Get torrents for item in queue.get('torrents', []): diff --git a/couchpotato/core/event.py b/couchpotato/core/event.py index bd704f5c..19e97541 100644 --- a/couchpotato/core/event.py +++ b/couchpotato/core/event.py @@ -110,7 +110,7 @@ def fireEvent(name, *args, **kwargs): if isinstance(results[0], dict): merged = {} for result in results: - merged = mergeDicts(merged, result) + merged = mergeDicts(merged, result, prepend_list = True) results = merged # Lists diff --git a/couchpotato/core/helpers/variable.py b/couchpotato/core/helpers/variable.py index 82bf88f7..ff1043bb 100644 --- a/couchpotato/core/helpers/variable.py +++ b/couchpotato/core/helpers/variable.py @@ -53,7 +53,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 +67,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] diff --git a/couchpotato/core/notifications/base.py b/couchpotato/core/notifications/base.py index 2604ef47..dda3dade 100644 --- a/couchpotato/core/notifications/base.py +++ b/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!' @@ -20,7 +22,7 @@ class Notification(Plugin): 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,13 +35,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(): + self.notify(*args, **kwargs) + def notify(self, message = '', data = {}, listener = None): pass @@ -49,7 +55,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' diff --git a/couchpotato/core/notifications/boxcar/main.py b/couchpotato/core/notifications/boxcar/main.py index 3135614a..b30d487a 100644 --- a/couchpotato/core/notifications/boxcar/main.py +++ b/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() diff --git a/couchpotato/core/notifications/email/main.py b/couchpotato/core/notifications/email/main.py index 014b5856..21fcf157 100644 --- a/couchpotato/core/notifications/email/main.py +++ b/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 diff --git a/couchpotato/core/notifications/growl/main.py b/couchpotato/core/notifications/growl/main.py index 7f1398d9..caad661b 100644 --- a/couchpotato/core/notifications/growl/main.py +++ b/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() diff --git a/couchpotato/core/notifications/notifo/main.py b/couchpotato/core/notifications/notifo/main.py index 64eee5ec..312055a8 100644 --- a/couchpotato/core/notifications/notifo/main.py +++ b/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 = { diff --git a/couchpotato/core/notifications/notifymyandroid/main.py b/couchpotato/core/notifications/notifymyandroid/main.py index cc6ef662..2c4ac90c 100644 --- a/couchpotato/core/notifications/notifymyandroid/main.py +++ b/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')) diff --git a/couchpotato/core/notifications/notifymywp/main.py b/couchpotato/core/notifications/notifymywp/main.py index 3258e85f..167b6eeb 100644 --- a/couchpotato/core/notifications/notifymywp/main.py +++ b/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')) diff --git a/couchpotato/core/notifications/plex/main.py b/couchpotato/core/notifications/plex/main.py index 9790ccfe..95139eed 100644 --- a/couchpotato/core/notifications/plex/main.py +++ b/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 diff --git a/couchpotato/core/notifications/prowl/main.py b/couchpotato/core/notifications/prowl/main.py index 0990d0d1..e5c4678b 100644 --- a/couchpotato/core/notifications/prowl/main.py +++ b/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'), diff --git a/couchpotato/core/notifications/pushalot/main.py b/couchpotato/core/notifications/pushalot/main.py index c35efa5c..4c5e76c3 100644 --- a/couchpotato/core/notifications/pushalot/main.py +++ b/couchpotato/core/notifications/pushalot/main.py @@ -12,7 +12,6 @@ class Pushalot(Notification): } def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return data = { 'AuthorizationToken': self.conf('auth_token'), diff --git a/couchpotato/core/notifications/pushover/main.py b/couchpotato/core/notifications/pushover/main.py index bcd3245c..8df4ed1f 100644 --- a/couchpotato/core/notifications/pushover/main.py +++ b/couchpotato/core/notifications/pushover/main.py @@ -11,7 +11,6 @@ class Pushover(Notification): app_token = 'YkxHMYDZp285L265L3IwH3LmzkTaCy' def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return http_handler = HTTPSConnection("api.pushover.net:443") diff --git a/couchpotato/core/notifications/toasty/main.py b/couchpotato/core/notifications/toasty/main.py index 638c75dd..79b021e2 100644 --- a/couchpotato/core/notifications/toasty/main.py +++ b/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, diff --git a/couchpotato/core/notifications/trakt/__init__.py b/couchpotato/core/notifications/trakt/__init__.py new file mode 100644 index 00000000..b119736c --- /dev/null +++ b/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 Automation Trakt settings', + 'options': [ + { + 'name': 'notification_enabled', + 'default': False, + 'type': 'enabler', + }, + { + 'name': 'remove_watchlist_enabled', + 'label': 'Remove from watchlist', + 'default': False, + 'type': 'bool', + }, + ], + } + ], +}] diff --git a/couchpotato/core/notifications/trakt/main.py b/couchpotato/core/notifications/trakt/main.py new file mode 100644 index 00000000..86d47086 --- /dev/null +++ b/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 diff --git a/couchpotato/core/notifications/twitter/main.py b/couchpotato/core/notifications/twitter/main.py index bbb9262b..2db19743 100644 --- a/couchpotato/core/notifications/twitter/main.py +++ b/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')) diff --git a/couchpotato/core/notifications/xbmc/main.py b/couchpotato/core/notifications/xbmc/main.py index 0187a833..84b9f7e4 100755 --- a/couchpotato/core/notifications/xbmc/main.py +++ b/couchpotato/core/notifications/xbmc/main.py @@ -15,7 +15,6 @@ class XBMC(Notification): use_json_notifications = {} def notify(self, message = '', data = {}, listener = None): - if self.isDisabled(): return hosts = splitString(self.conf('host')) diff --git a/couchpotato/core/plugins/movie/main.py b/couchpotato/core/plugins/movie/main.py index 5ec968c3..0a99fe6a 100644 --- a/couchpotato/core/plugins/movie/main.py +++ b/couchpotato/core/plugins/movie/main.py @@ -370,6 +370,7 @@ class MoviePlugin(Plugin): status_active = fireEvent('status.add', 'active', single = True) snatched_status = fireEvent('status.add', 'snatched', single = True) ignored_status = fireEvent('status.add', 'ignored', single = True) + done_status = fireEvent('status.add', 'done', single = True) downloaded_status = fireEvent('status.add', 'downloaded', single = True) default_profile = fireEvent('profile.default', single = True) @@ -397,7 +398,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: diff --git a/couchpotato/core/plugins/movie/static/movie.actions.js b/couchpotato/core/plugins/movie/static/movie.actions.js index a56e9abd..2b7f76d0 100644 --- a/couchpotato/core/plugins/movie/static/movie.actions.js +++ b/couchpotato/core/plugins/movie/static/movie.actions.js @@ -94,36 +94,30 @@ MA.Release = new Class({ } else { - var buttons_done = false; + var releases = self.movie.data.releases.sortBy('-info.score'); - self.movie.data.releases.sortBy('-info.score').each(function(release){ - if(buttons_done) return; - - var status = Status.get(release.status_id); + for(x in releases){ + var release = releases[x], + 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; + self.showHelper(); + break; } - - }); - + } } }, - 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 +232,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 +307,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 +338,8 @@ MA.Release = new Class({ tryNextRelease: function(movie_id){ var self = this; + self.createReleases(); + if(self.last_release) self.ignore(self.last_release); diff --git a/couchpotato/core/plugins/movie/static/movie.css b/couchpotato/core/plugins/movie/static/movie.css index 65fe5209..dd1bcbd3 100644 --- a/couchpotato/core/plugins/movie/static/movie.css +++ b/couchpotato/core/plugins/movie/static/movie.css @@ -39,7 +39,7 @@ overflow: hidden; width: 100%; height: 180px; - transition: all 0.2s linear; + transition: all 0.6s cubic-bezier(0.9,0,0.1,1); } .movies.list_list .movie:not(.details_view), @@ -95,7 +95,7 @@ width: 938px; box-shadow: none; border: 0; - background: none; + background: #4e5969; } .movies.thumbs_list .data { @@ -322,8 +322,8 @@ right: 10px; } - .movies .data:hover .action { opacity: 0.6; } - .movies .data:hover .action:hover { opacity: 1; } + .movies .movie:hover .action { opacity: 0.6; } + .movies .movie:hover .action:hover { opacity: 1; } .movies.mass_edit_list .data .actions { display: none; } @@ -338,8 +338,8 @@ opacity: 0; } - .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; @@ -510,6 +510,29 @@ .movies .movie .releases .last_release > :first-child { margin-left: -6px; } + .movies .movie .trynext { + display: inline; + position: absolute; + right: 135px; + z-index: 2; + opacity: 0; + text-shadow: none; + background: #4e5969; + } + .movies .movie:hover .trynext { + opacity: 1; + } + + .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:hover { + background-color: #369545; + } .movies .load_more { display: block; diff --git a/couchpotato/core/plugins/quality/main.py b/couchpotato/core/plugins/quality/main.py index 4cd5b8de..c32ad81d 100644 --- a/couchpotato/core/plugins/quality/main.py +++ b/couchpotato/core/plugins/quality/main.py @@ -19,8 +19,8 @@ class QualityPlugin(Plugin): qualities = [ {'identifier': 'bd50', 'hd': True, 'size': (15000, 60000), 'label': 'BR-Disk', 'alternative': ['bd25'], 'allow': ['1080p'], 'ext':[], 'tags': ['bdmv', 'certificate', ('complete', 'bluray')]}, - {'identifier': '1080p', 'hd': True, 'size': (5000, 20000), 'label': '1080P', 'width': 1920, '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')]}, diff --git a/couchpotato/core/plugins/release/main.py b/couchpotato/core/plugins/release/main.py index 02843f85..68bf60a6 100644 --- a/couchpotato/core/plugins/release/main.py +++ b/couchpotato/core/plugins/release/main.py @@ -160,7 +160,9 @@ class Release(Plugin): db = get_session() id = getParam('id') - status_snatched = fireEvent('status.add', 'snatched', single = True) + + snatched_status = fireEvent('status.add', 'snatched', single = True) + done_status = fireEvent('status.get', 'done', single = True) rel = db.query(Relea).filter_by(id = id).first() if rel: @@ -168,6 +170,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 +186,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 diff --git a/couchpotato/core/plugins/renamer/__init__.py b/couchpotato/core/plugins/renamer/__init__.py index 6ce21922..22277ba5 100644 --- a/couchpotato/core/plugins/renamer/__init__.py +++ b/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', diff --git a/couchpotato/core/plugins/renamer/main.py b/couchpotato/core/plugins/renamer/main.py index b7062157..aba20724 100644 --- a/couchpotato/core/plugins/renamer/main.py +++ b/couchpotato/core/plugins/renamer/main.py @@ -571,8 +571,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)) diff --git a/couchpotato/core/plugins/scanner/main.py b/couchpotato/core/plugins/scanner/main.py index b822bc0f..b2ac938c 100644 --- a/couchpotato/core/plugins/scanner/main.py +++ b/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 @@ -388,6 +389,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: diff --git a/couchpotato/core/plugins/searcher/main.py b/couchpotato/core/plugins/searcher/main.py index b4c4937a..a34b0b47 100644 --- a/couchpotato/core/plugins/searcher/main.py +++ b/couchpotato/core/plugins/searcher/main.py @@ -285,10 +285,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 +298,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 +342,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 diff --git a/couchpotato/core/plugins/subtitle/__init__.py b/couchpotato/core/plugins/subtitle/__init__.py index 686d385e..bbd40853 100644 --- a/couchpotato/core/plugins/subtitle/__init__.py +++ b/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 on Wikipedia', }, # { # 'name': 'automatic', diff --git a/couchpotato/core/plugins/trailer/__init__.py b/couchpotato/core/plugins/trailer/__init__.py index 282b3482..e4c51bd1 100644 --- a/couchpotato/core/plugins/trailer/__init__.py +++ b/couchpotato/core/plugins/trailer/__init__.py @@ -22,7 +22,7 @@ config = [{ 'name': 'quality', 'default': '720p', 'type': 'dropdown', - 'values': [('1080P', '1080p'), ('720P', '720p'), ('480P', '480p')], + 'values': [('1080p', '1080p'), ('720p', '720p'), ('480P', '480p')], }, { 'name': 'name', diff --git a/couchpotato/core/providers/automation/letterboxd/__init__.py b/couchpotato/core/providers/automation/letterboxd/__init__.py new file mode 100644 index 00000000..f2b8486b --- /dev/null +++ b/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 Letterboxd 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'], + }, + ], + }, + ], +}] diff --git a/couchpotato/core/providers/automation/letterboxd/main.py b/couchpotato/core/providers/automation/letterboxd/main.py new file mode 100644 index 00000000..7bae2ad5 --- /dev/null +++ b/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 diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py index 141ee4fe..6c8deb1e 100644 --- a/couchpotato/core/settings/model.py +++ b/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') diff --git a/couchpotato/runner.py b/couchpotato/runner.py index de538485..0ae14ce0 100644 --- a/couchpotato/runner.py +++ b/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 diff --git a/couchpotato/static/scripts/page/home.js b/couchpotato/static/scripts/page/home.js index c1e91162..004f0513 100644 --- a/couchpotato/static/scripts/page/home.js +++ b/couchpotato/static/scripts/page/home.js @@ -23,7 +23,7 @@ Page.Home = new Class({ 'identifier': 'snatched', 'load_more': false, 'view': 'list', - 'actions': [MA.IMDB, MA.Trailer, MA.Files, MA.Release, MA.Edit, MA.Readd, MA.Refresh, MA.Delete], + 'actions': [MA.IMDB, MA.Trailer, MA.Release, MA.Refresh, MA.Delete], 'title': 'Snatched & Available', 'on_empty_element': new Element('div'), 'filter': { diff --git a/couchpotato/static/scripts/page/settings.js b/couchpotato/static/scripts/page/settings.js index ddb2abf1..6e563bff 100644 --- a/couchpotato/static/scripts/page/settings.js +++ b/couchpotato/static/scripts/page/settings.js @@ -297,7 +297,7 @@ Page.Settings = new Class({ return group_el }, - + createList: function(content_container){ return new Element('div.option_list').grab( new Element('h3', { @@ -1283,6 +1283,7 @@ Option.Combined = new Class({ self.values = {} self.inputs = {} self.items = [] + self.labels = {} self.options.combine.each(function(name){ @@ -1302,9 +1303,10 @@ Option.Combined = new Class({ var head = new Element('div.head').inject(self.combined_list) Object.each(self.inputs, function(input, name){ + self.labels[name] = input.getPrevious().get('text') new Element('abbr', { 'class': name, - 'text': input.getPrevious().get('text'), + 'text': self.labels[name], //'title': input.getNext().get('text') }).inject(head) }) @@ -1367,7 +1369,7 @@ Option.Combined = new Class({ value_count++; new Element('input[type=text].inlay.'+name, { 'value': value, - 'placeholder': name, + 'placeholder': self.labels[name] || name, 'events': { 'keyup': self.saveCombined.bind(self), 'change': self.saveCombined.bind(self) diff --git a/couchpotato/static/style/main.css b/couchpotato/static/style/main.css index 3ad502b9..91bd1524 100644 --- a/couchpotato/static/style/main.css +++ b/couchpotato/static/style/main.css @@ -134,10 +134,9 @@ body > .spinner, .mask{ text-decoration: none; font-weight: bold; line-height: 1; - border-radius: 5px; - box-shadow: 0 1px 3px rgba(0,0,0,0.5); + border-radius: 2px; + box-shadow: 0 1px 2px rgba(0,0,0,0.3); text-shadow: 0 -1px 1px rgba(0,0,0,0.25); - border-bottom: 1px solid rgba(0,0,0,0.25); cursor: pointer; } .button.red { background-color: #ff0000; }