From e23af0e4623bfde1a6c411118d3c368c4504fd09 Mon Sep 17 00:00:00 2001 From: Supremicus Date: Tue, 17 Feb 2015 16:39:35 +1000 Subject: [PATCH 01/78] Update Pushbullet notifier (port from midgetspy/sickbeard) --- CHANGES.md | 1 + .../default/config_notifications.tmpl | 23 +-- gui/slick/js/configNotifications.js | 83 ++++---- sickbeard/__init__.py | 14 +- sickbeard/config.py | 9 +- sickbeard/notifiers/pushbullet.py | 181 ++++++++++-------- sickbeard/webserve.py | 16 +- 7 files changed, 176 insertions(+), 151 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index e7825ddc..197da80e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -60,6 +60,7 @@ * Add anime release groups to add new show options page * Add setting "Update shows during hour" to General Config/Misc * Add max-width to prevent ui glitch on Pull request and Branch Version selectors on config/General/Advanced and change tags to html5 +* Update Pushbullet notifier (port from midgetspy/sickbeard) [develop changelog] * Change uT params from unicode to str.format as magnet URLs worked but sending files in POST bodies failed diff --git a/gui/slick/interfaces/default/config_notifications.tmpl b/gui/slick/interfaces/default/config_notifications.tmpl index 7b8fff43..9efce083 100644 --- a/gui/slick/interfaces/default/config_notifications.tmpl +++ b/gui/slick/interfaces/default/config_notifications.tmpl @@ -1118,7 +1118,7 @@

Pushbullet

-

Pushbullet is a platform for receiving custom push notifications to connected devices running Android and desktop Chrome browsers.

+

Pushbullet allows you to send push notifications to Android, iOS and supported browsers.

@@ -1160,25 +1160,26 @@
-
-
Click below to test.
diff --git a/gui/slick/js/configNotifications.js b/gui/slick/js/configNotifications.js index 63830ac9..03c33428 100644 --- a/gui/slick/js/configNotifications.js +++ b/gui/slick/js/configNotifications.js @@ -377,68 +377,73 @@ $(document).ready(function(){ }); $('#testPushbullet').click(function () { - var pushbullet_api = $.trim($('#pushbullet_api').val()); - if (!pushbullet_api) { + var pushbullet_access_token = $.trim($('#pushbullet_access_token').val()); + var pushbullet_device_iden = $('#pushbullet_device_iden').val(); + if (!pushbullet_access_token) { $('#testPushbullet-result').html('Please fill out the necessary fields above.'); - $('#pushbullet_api').addClass('warning'); + $('#pushbullet_access_token').addClass('warning'); return; } - $('#pushbullet_api').removeClass('warning'); + $('#pushbullet_access_token').removeClass('warning'); $(this).prop('disabled', true); $('#testPushbullet-result').html(loading); - $.get(sbRoot + '/home/testPushbullet', {'api': pushbullet_api}) + $.get(sbRoot + '/home/testPushbullet', {'accessToken': pushbullet_access_token, 'device_iden': pushbullet_device_iden}) .done(function (data) { $('#testPushbullet-result').html(data); $('#testPushbullet').prop('disabled', false); }); }); - function get_pushbullet_devices(msg){ - - if(msg){ + function get_pushbullet_devices (msg) { + var pushbullet_access_token = $.trim($('#pushbullet_access_token').val()); + if (!pushbullet_access_token) { + $('#testPushbullet-result').html('Please fill out the necessary fields above.'); + $('#pushbullet_access_token').addClass('warning'); + return; + } + $(this).prop("disabled", true); + if (msg) { $('#testPushbullet-result').html(loading); } - - var pushbullet_api = $("#pushbullet_api").val(); - - if(!pushbullet_api) { - $('#testPushbullet-result').html("You didn't supply a Pushbullet api key"); - $("#pushbullet_api").focus(); - return false; - } - - var current_pushbullet_device = $("#pushbullet_device").val(); - $.get(sbRoot + "/home/getPushbulletDevices", {'api': pushbullet_api}, - function (data) { - var devices = jQuery.parseJSON(data).devices; - $("#pushbullet_device_list").html(''); - for (var i = 0; i < devices.length; i++) { - if(devices[i].active == true) { - if(current_pushbullet_device == devices[i].iden) { - $("#pushbullet_device_list").append('') - } else { - $("#pushbullet_device_list").append('') + var current_pushbullet_device = $('#pushbullet_device_iden').val(); + $.get(sbRoot + '/home/getPushbulletDevices', {'accessToken': pushbullet_access_token}) + .done(function (data) { + var devices = jQuery.parseJSON(data || '{}').devices; + $('#pushbullet_device_list').html(''); + // add default option to send to all devices + $('#pushbullet_device_list').append(''); + if (devices) { + for (var i = 0; i < devices.length; i++) { + // only list active device targets + if (devices[i].active == true) { + // if a device in the list matches our current iden, select it + if (current_pushbullet_device == devices[i].iden) { + $('#pushbullet_device_list').append(''); + } else { + $('#pushbullet_device_list').append(''); + } } } } - if(msg) { + $('#getPushbulletDevices').prop('disabled', false); + if (msg) { $('#testPushbullet-result').html(msg); } - } - ); + }); - $("#pushbullet_device_list").change(function(){ - $("#pushbullet_device").val($("#pushbullet_device_list").val()); - $('#testPushbullet-result').html("Don't forget to save your new pushbullet settings."); + $('#pushbullet_device_list').change(function () { + $('#pushbullet_device_iden').val($('#pushbullet_device_list').val()); + $('#testPushbullet-result').html('Don\'t forget to save your new Pushbullet settings.'); }); - }; + } - $('#getPushbulletDevices').click(function(){ - get_pushbullet_devices("Device list updated. Please choose a device to push to."); + $('#getPushbulletDevices').click(function () { + get_pushbullet_devices('Device list updated. Select specific device to use.'); }); - // we have to call this function on dom ready to create the devices select - get_pushbullet_devices(); + if ($('#use_pushbullet').prop('checked')) { + get_pushbullet_devices(); + } $('#email_show').change(function () { var key = parseInt($('#email_show').val(), 10); diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index ff02b2da..105c5c6a 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -394,8 +394,8 @@ USE_PUSHBULLET = False PUSHBULLET_NOTIFY_ONSNATCH = False PUSHBULLET_NOTIFY_ONDOWNLOAD = False PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = False -PUSHBULLET_API = None -PUSHBULLET_DEVICE = None +PUSHBULLET_ACCESS_TOKEN = None +PUSHBULLET_DEVICE_IDEN = None USE_EMAIL = False EMAIL_NOTIFY_ONSNATCH = False @@ -477,7 +477,7 @@ def initialize(consoleLogging=True): USE_PYTIVO, PYTIVO_NOTIFY_ONSNATCH, PYTIVO_NOTIFY_ONDOWNLOAD, PYTIVO_NOTIFY_ONSUBTITLEDOWNLOAD, PYTIVO_UPDATE_LIBRARY, PYTIVO_HOST, PYTIVO_SHARE_NAME, PYTIVO_TIVO_NAME, \ USE_NMA, NMA_NOTIFY_ONSNATCH, NMA_NOTIFY_ONDOWNLOAD, NMA_NOTIFY_ONSUBTITLEDOWNLOAD, NMA_API, NMA_PRIORITY, \ USE_PUSHALOT, PUSHALOT_NOTIFY_ONSNATCH, PUSHALOT_NOTIFY_ONDOWNLOAD, PUSHALOT_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHALOT_AUTHORIZATIONTOKEN, \ - USE_PUSHBULLET, PUSHBULLET_NOTIFY_ONSNATCH, PUSHBULLET_NOTIFY_ONDOWNLOAD, PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHBULLET_API, PUSHBULLET_DEVICE, \ + USE_PUSHBULLET, PUSHBULLET_NOTIFY_ONSNATCH, PUSHBULLET_NOTIFY_ONDOWNLOAD, PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD, PUSHBULLET_ACCESS_TOKEN, PUSHBULLET_DEVICE_IDEN, \ versionCheckScheduler, VERSION_NOTIFY, AUTO_UPDATE, NOTIFY_ON_UPDATE, PROCESS_AUTOMATICALLY, UNPACK, CPU_PRESET, \ KEEP_PROCESSED_DIR, PROCESS_METHOD, TV_DOWNLOAD_DIR, MIN_RECENTSEARCH_FREQUENCY, DEFAULT_UPDATE_FREQUENCY, MIN_UPDATE_FREQUENCY, UPDATE_FREQUENCY, \ showQueueScheduler, searchQueueScheduler, ROOT_DIRS, CACHE_DIR, ACTUAL_CACHE_DIR, TIMEZONE_DISPLAY, \ @@ -870,8 +870,8 @@ def initialize(consoleLogging=True): PUSHBULLET_NOTIFY_ONDOWNLOAD = bool(check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_ondownload', 0)) PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = bool( check_setting_int(CFG, 'Pushbullet', 'pushbullet_notify_onsubtitledownload', 0)) - PUSHBULLET_API = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '') - PUSHBULLET_DEVICE = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device', '') + PUSHBULLET_ACCESS_TOKEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_access_token', '') + PUSHBULLET_DEVICE_IDEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device_iden', '') USE_EMAIL = bool(check_setting_int(CFG, 'Email', 'use_email', 0)) EMAIL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Email', 'email_notify_onsnatch', 0)) @@ -1719,8 +1719,8 @@ def save_config(): new_config['Pushbullet']['pushbullet_notify_onsnatch'] = int(PUSHBULLET_NOTIFY_ONSNATCH) new_config['Pushbullet']['pushbullet_notify_ondownload'] = int(PUSHBULLET_NOTIFY_ONDOWNLOAD) new_config['Pushbullet']['pushbullet_notify_onsubtitledownload'] = int(PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD) - new_config['Pushbullet']['pushbullet_api'] = PUSHBULLET_API - new_config['Pushbullet']['pushbullet_device'] = PUSHBULLET_DEVICE + new_config['Pushbullet']['pushbullet_access_token'] = PUSHBULLET_ACCESS_TOKEN + new_config['Pushbullet']['pushbullet_device_iden'] = PUSHBULLET_DEVICE_IDEN new_config['Email'] = {} new_config['Email']['use_email'] = int(USE_EMAIL) diff --git a/sickbeard/config.py b/sickbeard/config.py index a50dc5b0..576bcb9c 100644 --- a/sickbeard/config.py +++ b/sickbeard/config.py @@ -449,8 +449,9 @@ class ConfigMigrator(): 4: 'Add newznab catIDs', 5: 'Metadata update', 6: 'Rename daily search to recent search', - 7: 'Rename coming episodes to episode view' - } + 7: 'Rename coming episodes to episode view', + 8: 'Disable searches on start', + 9: 'Rename pushbullet variables',} def migrate_config(self): """ @@ -745,3 +746,7 @@ class ConfigMigrator(): # removing settings from gui and making it a hidden debug option sickbeard.RECENTSEARCH_STARTUP = False sickbeard.BACKLOG_STARTUP = False + + def _migrate_v9(self): + sickbeard.PUSHBULLET_ACCESS_TOKEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_api', '') + sickbeard.PUSHBULLET_DEVICE_IDEN = check_setting_str(CFG, 'Pushbullet', 'pushbullet_device', '') \ No newline at end of file diff --git a/sickbeard/notifiers/pushbullet.py b/sickbeard/notifiers/pushbullet.py index 76fc81c0..8aa3e97f 100644 --- a/sickbeard/notifiers/pushbullet.py +++ b/sickbeard/notifiers/pushbullet.py @@ -1,5 +1,4 @@ -# Author: Pedro Correia (http://github.com/pedrocorreia/) -# Based on pushalot.py by Nic Wolfe +# Author: Nic Wolfe # URL: http://code.google.com/p/sickbeard/ # # This file is part of SickGear. @@ -17,109 +16,123 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . +import urllib +import urllib2 import socket import base64 -from httplib import HTTPSConnection, HTTPException -import json -from ssl import SSLError + import sickbeard -from sickbeard import logger, common + +from sickbeard import logger +from sickbeard.common import notifyStrings, NOTIFY_SNATCH, NOTIFY_DOWNLOAD, NOTIFY_SUBTITLE_DOWNLOAD, NOTIFY_GIT_UPDATE, NOTIFY_GIT_UPDATE_TEXT +from sickbeard.exceptions import ex + +PUSHAPI_ENDPOINT = 'https://api.pushbullet.com/v2/pushes' +DEVICEAPI_ENDPOINT = 'https://api.pushbullet.com/v2/devices' class PushbulletNotifier: - def test_notify(self, pushbullet_api): - return self._sendPushbullet(pushbullet_api, event="Test", message="Testing Pushbullet settings from SickGear", - method="POST", notificationType="note", force=True) - def get_devices(self, pushbullet_api): - return self._sendPushbullet(pushbullet_api, method="GET", force=True) + def get_devices(self, accessToken=None): + # fill in omitted parameters + if not accessToken: + accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN - def notify_snatch(self, ep_name): - if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH: - self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SNATCH] + " : " + ep_name, message=ep_name, - notificationType="note", method="POST") + # get devices from pushbullet + try: + req = urllib2.Request(DEVICEAPI_ENDPOINT) + base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1] + req.add_header('Authorization', 'Basic %s' % base64string) + handle = urllib2.urlopen(req) + if handle: + result = handle.read() + handle.close() + return result + except urllib2.URLError: + return None + except socket.timeout: + return None - def notify_download(self, ep_name): - if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD: - self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_DOWNLOAD] + " : " + ep_name, - message=ep_name, notificationType="note", method="POST") + def _sendPushbullet(self, title, body, accessToken, device_iden): - def notify_subtitle_download(self, ep_name, lang): - if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: - self._sendPushbullet(pushbullet_api=None, event=common.notifyStrings[common.NOTIFY_SUBTITLE_DOWNLOAD] + " : " + ep_name + " : " + lang, - message=ep_name + ": " + lang, notificationType="note", method="POST") - - def notify_git_update(self, new_version = "??"): - if sickbeard.USE_PUSHBULLET: - update_text=common.notifyStrings[common.NOTIFY_GIT_UPDATE_TEXT] - title=common.notifyStrings[common.NOTIFY_GIT_UPDATE] - self._sendPushbullet(pushbullet_api=None, event=title, message=update_text + new_version, method="POST") + # build up the URL and parameters + body = body.strip().encode('utf-8') - def _sendPushbullet(self, pushbullet_api=None, pushbullet_device=None, event=None, message=None, - notificationType=None, method=None, force=False): + data = urllib.urlencode({ + 'type': 'note', + 'title': title, + 'body': body, + 'device_iden': device_iden + }) + # send the request to pushbullet + try: + req = urllib2.Request(PUSHAPI_ENDPOINT) + base64string = base64.encodestring('%s:%s' % (accessToken, ''))[:-1] + req.add_header('Authorization', 'Basic %s' % base64string) + handle = urllib2.urlopen(req, data) + handle.close() + except socket.timeout: + return False + except urllib2.URLError, e: + + if e.code == 404: + logger.log(u'PUSHBULLET: Access token is wrong/not associated to a device.', logger.ERROR) + elif e.code == 401: + logger.log(u'PUSHBULLET: Unauthorized, not a valid access token.', logger.ERROR) + elif e.code == 400: + logger.log(u'PUSHBULLET: Bad request, missing required parameter.', logger.ERROR) + elif e.code == 503: + logger.log(u'PUSHBULLET: Pushbullet server to busy to handle the request at this time.', logger.WARNING) + return False + + logger.log(u'PUSHBULLET: Notification successful.', logger.MESSAGE) + return True + + def _notifyPushbullet(self, title, body, accessToken=None, device_iden=None, force=False): + """ + Sends a pushbullet notification based on the provided info or SG config + + title: The title of the notification to send + body: The body string to send + accessToken: The access token to grant access + device_iden: The iden of a specific target, if none provided send to all devices + force: If True then the notification will be sent even if Pushbullet is disabled in the config + """ + + # suppress notifications if the notifier is disabled but the notify options are checked if not sickbeard.USE_PUSHBULLET and not force: return False - if pushbullet_api == None: - pushbullet_api = sickbeard.PUSHBULLET_API - if pushbullet_device == None: - pushbullet_device = sickbeard.PUSHBULLET_DEVICE + # fill in omitted parameters + if not accessToken: + accessToken = sickbeard.PUSHBULLET_ACCESS_TOKEN + if not device_iden: + device_iden = sickbeard.PUSHBULLET_DEVICE_IDEN - if method == 'POST': - uri = '/v2/pushes' - else: - uri = '/v2/devices' + logger.log(u'PUSHBULLET: Sending notice with details: \"%s - %s\", device_iden: %s' % (title, body, device_iden), logger.DEBUG) - logger.log(u"Pushbullet event: " + str(event), logger.DEBUG) - logger.log(u"Pushbullet message: " + str(message), logger.DEBUG) - logger.log(u"Pushbullet api: " + str(pushbullet_api), logger.DEBUG) - logger.log(u"Pushbullet devices: " + str(pushbullet_device), logger.DEBUG) - logger.log(u"Pushbullet notification type: " + str(notificationType), logger.DEBUG) + return self._sendPushbullet(title, body, accessToken, device_iden) - http_handler = HTTPSConnection("api.pushbullet.com") + def notify_snatch(self, ep_name): + if sickbeard.PUSHBULLET_NOTIFY_ONSNATCH: + self._notifyPushbullet(notifyStrings[NOTIFY_SNATCH], ep_name) - if notificationType == None: - testMessage = True - try: - logger.log(u"Testing Pushbullet authentication and retrieving the device list.", logger.DEBUG) - http_handler.request(method, uri, None, headers={'Authorization': 'Bearer %s' % pushbullet_api}) - except (SSLError, HTTPException, socket.error): - logger.log(u"Pushbullet notification failed.", logger.ERROR) - return False - else: - testMessage = False - try: - data = { - 'title': event.encode('utf-8'), - 'body': message.encode('utf-8'), - 'device_iden': pushbullet_device, - 'type': notificationType} - data = json.dumps(data) - http_handler.request(method, uri, body=data, - headers={'Content-Type': 'application/json', 'Authorization': 'Bearer %s' % pushbullet_api}) - pass - except (SSLError, HTTPException, socket.error): - return False + def notify_download(self, ep_name): + if sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD: + self._notifyPushbullet(notifyStrings[NOTIFY_DOWNLOAD], ep_name) - response = http_handler.getresponse() - request_body = response.read() - request_status = response.status - logger.log(u"Pushbullet response: %s" % request_body, logger.DEBUG) + def notify_subtitle_download(self, ep_name, lang): + if sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD: + self._notifyPushbullet(notifyStrings[NOTIFY_SUBTITLE_DOWNLOAD], ep_name + ': ' + lang) - if request_status == 200: - if testMessage: - return request_body - else: - logger.log(u"Pushbullet notifications sent.", logger.DEBUG) - return True - elif request_status == 410: - logger.log(u"Pushbullet authentication failed: %s" % response.reason, logger.ERROR) - return False - else: - logger.log(u"Pushbullet notification failed.", logger.ERROR) - return False + def notify_git_update(self, new_version = '??'): + if sickbeard.USE_PUSHBULLET: + update_text=notifyStrings[NOTIFY_GIT_UPDATE_TEXT] + title=notifyStrings[NOTIFY_GIT_UPDATE] + self._notifyPushbullet(title, update_text + new_version) + def test_notify(self, accessToken, device_iden): + return self._notifyPushbullet('Test', 'This is a test notification from SickGear', accessToken, device_iden, force=True) notifier = PushbulletNotifier - diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index ed7ef2b8..4892daaf 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -2354,8 +2354,8 @@ class ConfigNotifications(MainHandler): use_pushalot=None, pushalot_notify_onsnatch=None, pushalot_notify_ondownload=None, pushalot_notify_onsubtitledownload=None, pushalot_authorizationtoken=None, use_pushbullet=None, pushbullet_notify_onsnatch=None, pushbullet_notify_ondownload=None, - pushbullet_notify_onsubtitledownload=None, pushbullet_api=None, pushbullet_device=None, - pushbullet_device_list=None, + pushbullet_notify_onsubtitledownload=None, pushbullet_access_token=None, + pushbullet_device_iden=None, pushbullet_device_list=None, use_email=None, email_notify_onsnatch=None, email_notify_ondownload=None, email_notify_onsubtitledownload=None, email_host=None, email_port=25, email_from=None, email_tls=None, email_user=None, email_password=None, email_list=None, email_show_list=None, @@ -2497,8 +2497,8 @@ class ConfigNotifications(MainHandler): sickbeard.PUSHBULLET_NOTIFY_ONSNATCH = config.checkbox_to_value(pushbullet_notify_onsnatch) sickbeard.PUSHBULLET_NOTIFY_ONDOWNLOAD = config.checkbox_to_value(pushbullet_notify_ondownload) sickbeard.PUSHBULLET_NOTIFY_ONSUBTITLEDOWNLOAD = config.checkbox_to_value(pushbullet_notify_onsubtitledownload) - sickbeard.PUSHBULLET_API = pushbullet_api - sickbeard.PUSHBULLET_DEVICE = pushbullet_device_list + sickbeard.PUSHBULLET_ACCESS_TOKEN = pushbullet_access_token + sickbeard.PUSHBULLET_DEVICE_IDEN = pushbullet_device_iden sickbeard.save_config() @@ -3554,20 +3554,20 @@ class Home(MainHandler): return "Error sending Pushalot notification" - def testPushbullet(self, api=None): + def testPushbullet(self, accessToken=None, device_iden=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - result = notifiers.pushbullet_notifier.test_notify(api) + result = notifiers.pushbullet_notifier.test_notify(accessToken, device_iden) if result: return "Pushbullet notification succeeded. Check your device to make sure it worked" else: return "Error sending Pushbullet notification" - def getPushbulletDevices(self, api=None): + def getPushbulletDevices(self, accessToken=None): self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - result = notifiers.pushbullet_notifier.get_devices(api) + result = notifiers.pushbullet_notifier.get_devices(accessToken) if result: return result else: From 12cc540c5859eb008c11d486219dc0ab59500631 Mon Sep 17 00:00:00 2001 From: Adam Date: Fri, 20 Feb 2015 23:12:04 +0800 Subject: [PATCH 02/78] Add requirements file for pip (port from midgetspy/sick-beard) Allows for use of 'pip install -r requirements.txt' to install prerequisites for SickGear to run --- CHANGES.md | 7 +++++++ requirements.txt | 1 + 2 files changed, 8 insertions(+) create mode 100644 requirements.txt diff --git a/CHANGES.md b/CHANGES.md index ba57b5e9..2890a364 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,10 @@ +### 0.x.x (2015-xx-xx xx:xx:xx UTC) + +* Add requirements file for pip (port from midgetspy/sick-beard) + +[develop changelog] + + ### 0.7.0 (2015-02-23 11:02:00 UTC) * Fix slow database operations (port from midgetspy/sickbeard) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..a3ac7ffb --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +Cheetah>=2.1.0 \ No newline at end of file From c4908144e1ae745b08fbe3d4ea84f38a95b87afb Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 20 Feb 2015 21:30:23 +0800 Subject: [PATCH 03/78] Remove unused libraries fuzzywuzzy and pysrt --- CHANGES.md | 1 + lib/fuzzywuzzy/StringMatcher.py | 78 ------- lib/fuzzywuzzy/__init__.py | 0 lib/fuzzywuzzy/fuzz.py | 263 ----------------------- lib/fuzzywuzzy/process.py | 119 ----------- lib/fuzzywuzzy/string_processing.py | 41 ---- lib/fuzzywuzzy/utils.py | 76 ------- lib/pysrt/__init__.py | 18 -- lib/pysrt/commands.py | 218 ------------------- lib/pysrt/comparablemixin.py | 26 --- lib/pysrt/compat.py | 24 --- lib/pysrt/srtexc.py | 31 --- lib/pysrt/srtfile.py | 312 ---------------------------- lib/pysrt/srtitem.py | 76 ------- lib/pysrt/srttime.py | 176 ---------------- lib/pysrt/version.py | 2 - 16 files changed, 1 insertion(+), 1460 deletions(-) delete mode 100644 lib/fuzzywuzzy/StringMatcher.py delete mode 100644 lib/fuzzywuzzy/__init__.py delete mode 100644 lib/fuzzywuzzy/fuzz.py delete mode 100644 lib/fuzzywuzzy/process.py delete mode 100644 lib/fuzzywuzzy/string_processing.py delete mode 100644 lib/fuzzywuzzy/utils.py delete mode 100644 lib/pysrt/__init__.py delete mode 100644 lib/pysrt/commands.py delete mode 100644 lib/pysrt/comparablemixin.py delete mode 100644 lib/pysrt/compat.py delete mode 100644 lib/pysrt/srtexc.py delete mode 100644 lib/pysrt/srtfile.py delete mode 100644 lib/pysrt/srtitem.py delete mode 100644 lib/pysrt/srttime.py delete mode 100644 lib/pysrt/version.py diff --git a/CHANGES.md b/CHANGES.md index 2890a364..0f1d30df 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,7 @@ ### 0.x.x (2015-xx-xx xx:xx:xx UTC) * Add requirements file for pip (port from midgetspy/sick-beard) +* Remove unused libraries fuzzywuzzy and pysrt [develop changelog] diff --git a/lib/fuzzywuzzy/StringMatcher.py b/lib/fuzzywuzzy/StringMatcher.py deleted file mode 100644 index 9dccfe7e..00000000 --- a/lib/fuzzywuzzy/StringMatcher.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -""" -StringMatcher.py - -ported from python-Levenshtein -[https://github.com/miohtama/python-Levenshtein] -""" - -from Levenshtein import * -from warnings import warn - -class StringMatcher: - """A SequenceMatcher-like class built on the top of Levenshtein""" - - def _reset_cache(self): - self._ratio = self._distance = None - self._opcodes = self._editops = self._matching_blocks = None - - def __init__(self, isjunk=None, seq1='', seq2=''): - if isjunk: - warn("isjunk not NOT implemented, it will be ignored") - self._str1, self._str2 = seq1, seq2 - self._reset_cache() - - def set_seqs(self, seq1, seq2): - self._str1, self._str2 = seq1, seq2 - self._reset_cache() - - def set_seq1(self, seq1): - self._str1 = seq1 - self._reset_cache() - - def set_seq2(self, seq2): - self._str2 = seq2 - self._reset_cache() - - def get_opcodes(self): - if not self._opcodes: - if self._editops: - self._opcodes = opcodes(self._editops, self._str1, self._str2) - else: - self._opcodes = opcodes(self._str1, self._str2) - return self._opcodes - - def get_editops(self): - if not self._editops: - if self._opcodes: - self._editops = editops(self._opcodes, self._str1, self._str2) - else: - self._editops = editops(self._str1, self._str2) - return self._editops - - def get_matching_blocks(self): - if not self._matching_blocks: - self._matching_blocks = matching_blocks(self.get_opcodes(), - self._str1, self._str2) - return self._matching_blocks - - def ratio(self): - if not self._ratio: - self._ratio = ratio(self._str1, self._str2) - return self._ratio - - def quick_ratio(self): - # This is usually quick enough :o) - if not self._ratio: - self._ratio = ratio(self._str1, self._str2) - return self._ratio - - def real_quick_ratio(self): - len1, len2 = len(self._str1), len(self._str2) - return 2.0 * min(len1, len2) / (len1 + len2) - - def distance(self): - if not self._distance: - self._distance = distance(self._str1, self._str2) - return self._distance \ No newline at end of file diff --git a/lib/fuzzywuzzy/__init__.py b/lib/fuzzywuzzy/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lib/fuzzywuzzy/fuzz.py b/lib/fuzzywuzzy/fuzz.py deleted file mode 100644 index 26274b9a..00000000 --- a/lib/fuzzywuzzy/fuzz.py +++ /dev/null @@ -1,263 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -""" -fuzz.py - -Copyright (c) 2011 Adam Cohen - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" -from __future__ import unicode_literals - -try: - from StringMatcher import StringMatcher as SequenceMatcher -except: - from difflib import SequenceMatcher - -from . import utils - - -########################### -# Basic Scoring Functions # -########################### - - -def ratio(s1, s2): - - if s1 is None: - raise TypeError("s1 is None") - if s2 is None: - raise TypeError("s2 is None") - s1, s2 = utils.make_type_consistent(s1, s2) - if len(s1) == 0 or len(s2) == 0: - return 0 - - m = SequenceMatcher(None, s1, s2) - return utils.intr(100 * m.ratio()) - - -# todo: skip duplicate indexes for a little more speed -def partial_ratio(s1, s2): - - if s1 is None: - raise TypeError("s1 is None") - if s2 is None: - raise TypeError("s2 is None") - s1, s2 = utils.make_type_consistent(s1, s2) - if len(s1) == 0 or len(s2) == 0: - return 0 - - if len(s1) <= len(s2): - shorter = s1 - longer = s2 - else: - shorter = s2 - longer = s1 - - m = SequenceMatcher(None, shorter, longer) - blocks = m.get_matching_blocks() - - # each block represents a sequence of matching characters in a string - # of the form (idx_1, idx_2, len) - # the best partial match will block align with at least one of those blocks - # e.g. shorter = "abcd", longer = XXXbcdeEEE - # block = (1,3,3) - # best score === ratio("abcd", "Xbcd") - scores = [] - for block in blocks: - long_start = block[1] - block[0] if (block[1] - block[0]) > 0 else 0 - long_end = long_start + len(shorter) - long_substr = longer[long_start:long_end] - - m2 = SequenceMatcher(None, shorter, long_substr) - r = m2.ratio() - if r > .995: - return 100 - else: - scores.append(r) - - return int(100 * max(scores)) - - -############################## -# Advanced Scoring Functions # -############################## - -# Sorted Token -# find all alphanumeric tokens in the string -# sort those tokens and take ratio of resulting joined strings -# controls for unordered string elements -def _token_sort(s1, s2, partial=True, force_ascii=True): - - if s1 is None: - raise TypeError("s1 is None") - if s2 is None: - raise TypeError("s2 is None") - - # pull tokens - tokens1 = utils.full_process(s1, force_ascii=force_ascii).split() - tokens2 = utils.full_process(s2, force_ascii=force_ascii).split() - - # sort tokens and join - sorted1 = " ".join(sorted(tokens1)) - sorted2 = " ".join(sorted(tokens2)) - - sorted1 = sorted1.strip() - sorted2 = sorted2.strip() - - if partial: - return partial_ratio(sorted1, sorted2) - else: - return ratio(sorted1, sorted2) - - -def token_sort_ratio(s1, s2, force_ascii=True): - return _token_sort(s1, s2, partial=False, force_ascii=force_ascii) - - -def partial_token_sort_ratio(s1, s2, force_ascii=True): - return _token_sort(s1, s2, partial=True, force_ascii=force_ascii) - - -# Token Set -# find all alphanumeric tokens in each string...treat them as a set -# construct two strings of the form -# -# take ratios of those two strings -# controls for unordered partial matches -def _token_set(s1, s2, partial=True, force_ascii=True): - - if s1 is None: - raise TypeError("s1 is None") - if s2 is None: - raise TypeError("s2 is None") - - p1 = utils.full_process(s1, force_ascii=force_ascii) - p2 = utils.full_process(s2, force_ascii=force_ascii) - - if not utils.validate_string(p1): - return 0 - if not utils.validate_string(p2): - return 0 - - # pull tokens - tokens1 = set(utils.full_process(p1).split()) - tokens2 = set(utils.full_process(p2).split()) - - intersection = tokens1.intersection(tokens2) - diff1to2 = tokens1.difference(tokens2) - diff2to1 = tokens2.difference(tokens1) - - sorted_sect = " ".join(sorted(intersection)) - sorted_1to2 = " ".join(sorted(diff1to2)) - sorted_2to1 = " ".join(sorted(diff2to1)) - - combined_1to2 = sorted_sect + " " + sorted_1to2 - combined_2to1 = sorted_sect + " " + sorted_2to1 - - # strip - sorted_sect = sorted_sect.strip() - combined_1to2 = combined_1to2.strip() - combined_2to1 = combined_2to1.strip() - - pairwise = [ - ratio(sorted_sect, combined_1to2), - ratio(sorted_sect, combined_2to1), - ratio(combined_1to2, combined_2to1) - ] - return max(pairwise) - - -def token_set_ratio(s1, s2, force_ascii=True): - return _token_set(s1, s2, partial=False, force_ascii=force_ascii) - - -def partial_token_set_ratio(s1, s2, force_ascii=True): - return _token_set(s1, s2, partial=True, force_ascii=force_ascii) - - -# TODO: numerics - -################### -# Combination API # -################### - -# q is for quick -def QRatio(s1, s2, force_ascii=True): - - p1 = utils.full_process(s1, force_ascii=force_ascii) - p2 = utils.full_process(s2, force_ascii=force_ascii) - - if not utils.validate_string(p1): - return 0 - if not utils.validate_string(p2): - return 0 - - return ratio(p1, p2) - - -def UQRatio(s1, s2): - return QRatio(s1, s2, force_ascii=False) - - -# w is for weighted -def WRatio(s1, s2, force_ascii=True): - - p1 = utils.full_process(s1, force_ascii=force_ascii) - p2 = utils.full_process(s2, force_ascii=force_ascii) - - if not utils.validate_string(p1): - return 0 - if not utils.validate_string(p2): - return 0 - - # should we look at partials? - try_partial = True - unbase_scale = .95 - partial_scale = .90 - - base = ratio(p1, p2) - len_ratio = float(max(len(p1), len(p2))) / min(len(p1), len(p2)) - - # if strings are similar length, don't use partials - if len_ratio < 1.5: - try_partial = False - - # if one string is much much shorter than the other - if len_ratio > 8: - partial_scale = .6 - - if try_partial: - partial = partial_ratio(p1, p2) * partial_scale - ptsor = partial_token_sort_ratio(p1, p2, force_ascii=force_ascii) \ - * unbase_scale * partial_scale - ptser = partial_token_set_ratio(p1, p2, force_ascii=force_ascii) \ - * unbase_scale * partial_scale - - return int(max(base, partial, ptsor, ptser)) - else: - tsor = token_sort_ratio(p1, p2, force_ascii=force_ascii) * unbase_scale - tser = token_set_ratio(p1, p2, force_ascii=force_ascii) * unbase_scale - - return int(max(base, tsor, tser)) - - -def UWRatio(s1, s2): - return WRatio(s1, s2, force_ascii=False) diff --git a/lib/fuzzywuzzy/process.py b/lib/fuzzywuzzy/process.py deleted file mode 100644 index 7571664e..00000000 --- a/lib/fuzzywuzzy/process.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -""" -process.py - -Copyright (c) 2011 Adam Cohen - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -""" -import itertools - -from . import fuzz -from . import utils - - -def extract(query, choices, processor=None, scorer=None, limit=5): - """Find best matches in a list of choices, return a list of tuples - containing the match and it's score. - - Arguments: - query -- an object representing the thing we want to find - choices -- a list of objects we are attempting to extract - values from - scorer -- f(OBJ, QUERY) --> INT. We will return the objects - with the highest score by default, we use - score.WRatio() and both OBJ and QUERY should be - strings - processor -- f(OBJ_A) --> OBJ_B, where the output is an input - to scorer for example, "processor = lambda x: - x[0]" would return the first element in a - collection x (of, say, strings) this would then - be used in the scoring collection by default, we - use utils.full_process() - - """ - if choices is None or len(choices) == 0: - return [] - - # default, turn whatever the choice is into a workable string - if processor is None: - processor = lambda x: utils.full_process(x) - - # default: wratio - if scorer is None: - scorer = fuzz.WRatio - - sl = list() - - for choice in choices: - processed = processor(choice) - score = scorer(query, processed) - tuple = (choice, score) - sl.append(tuple) - - sl.sort(key=lambda i: i[1], reverse=True) - return sl[:limit] - - -def extractBests(query, choices, processor=None, scorer=None, score_cutoff=0, limit=5): - """Find best matches above a score in a list of choices, return a - list of tuples containing the match and it's score. - - Convenience method which returns the choices with best scores, see - extract() for full arguments list - - Optional parameter: score_cutoff. - If the choice has a score of less than or equal to score_cutoff - it will not be included on result list - - """ - - best_list = extract(query, choices, processor, scorer, limit) - if len(best_list) > 0: - return list(itertools.takewhile(lambda x: x[1] > score_cutoff, best_list)) - else: - return [] - - -def extractOne(query, choices, processor=None, scorer=None, score_cutoff=0): - """Find the best match above a score in a list of choices, return a - tuple containing the match and it's score if it's above the treshold - or None. - - Convenience method which returns the single best choice, see - extract() for full arguments list - - Optional parameter: score_cutoff. - If the best choice has a score of less than or equal to - score_cutoff we will return none (intuition: not a good enough - match) - - """ - - best_list = extract(query, choices, processor, scorer, limit=1) - if len(best_list) > 0: - best = best_list[0] - if best[1] > score_cutoff: - return best - else: - return None - else: - return None diff --git a/lib/fuzzywuzzy/string_processing.py b/lib/fuzzywuzzy/string_processing.py deleted file mode 100644 index 7c706d98..00000000 --- a/lib/fuzzywuzzy/string_processing.py +++ /dev/null @@ -1,41 +0,0 @@ -from __future__ import unicode_literals -import re - - -class StringProcessor(object): - """ - This class defines method to process strings in the most - efficient way. Ideally all the methods below use unicode strings - for both input and output. - """ - - @classmethod - def replace_non_letters_non_numbers_with_whitespace(cls, a_string): - """ - This function replaces any sequence of non letters and non - numbers with a single white space. - """ - regex = re.compile(r"(?ui)\W") - return regex.sub(" ", a_string) - - @classmethod - def strip(cls, a_string): - """ - This function strips leading and trailing white space. - """ - - return a_string.strip() - - @classmethod - def to_lower_case(cls, a_string): - """ - This function returns the lower-cased version of the string given. - """ - return a_string.lower() - - @classmethod - def to_upper_case(cls, a_string): - """ - This function returns the upper-cased version of the string given. - """ - return a_string.upper() diff --git a/lib/fuzzywuzzy/utils.py b/lib/fuzzywuzzy/utils.py deleted file mode 100644 index 2d3ae3e4..00000000 --- a/lib/fuzzywuzzy/utils.py +++ /dev/null @@ -1,76 +0,0 @@ -from __future__ import unicode_literals -import sys - -from fuzzywuzzy.string_processing import StringProcessor - - -PY3 = sys.version_info[0] == 3 - - -def validate_string(s): - try: - if len(s) > 0: - return True - else: - return False - except: - return False - -bad_chars = str('') # ascii dammit! -for i in range(128, 256): - bad_chars += chr(i) -if PY3: - translation_table = dict((ord(c), None) for c in bad_chars) - - -def asciionly(s): - if PY3: - return s.translate(translation_table) - else: - return s.translate(None, bad_chars) - - -def asciidammit(s): - if type(s) is str: - return asciionly(s) - elif type(s) is unicode: - return asciionly(s.encode('ascii', 'ignore')) - else: - return asciidammit(unicode(s)) - - -def make_type_consistent(s1, s2): - if isinstance(s1, str) and isinstance(s2, str): - return s1, s2 - - elif isinstance(s1, unicode) and isinstance(s2, unicode): - return s1, s2 - - else: - return unicode(s1), unicode(s2) - - -def full_process(s, force_ascii=False): - """Process string by - -- removing all but letters and numbers - -- trim whitespace - -- force to lower case - if force_ascii == True, force convert to ascii""" - - if s is None: - return "" - - if force_ascii: - s = asciidammit(s) - # Keep only Letters and Numbres (see Unicode docs). - string_out = StringProcessor.replace_non_letters_non_numbers_with_whitespace(s) - # Force into lowercase. - string_out = StringProcessor.to_lower_case(string_out) - # Remove leading and trailing whitespaces. - string_out = StringProcessor.strip(string_out) - return string_out - - -def intr(n): - '''Returns a correctly rounded integer''' - return int(round(n)) diff --git a/lib/pysrt/__init__.py b/lib/pysrt/__init__.py deleted file mode 100644 index 34e96717..00000000 --- a/lib/pysrt/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -from pysrt.srttime import SubRipTime -from pysrt.srtitem import SubRipItem -from pysrt.srtfile import SubRipFile -from pysrt.srtexc import Error, InvalidItem, InvalidTimeString -from pysrt.version import VERSION, VERSION_STRING - -__all__ = [ - 'SubRipFile', 'SubRipItem', 'SubRipFile', 'SUPPORT_UTF_32_LE', - 'SUPPORT_UTF_32_BE', 'InvalidItem', 'InvalidTimeString' -] - -ERROR_PASS = SubRipFile.ERROR_PASS -ERROR_LOG = SubRipFile.ERROR_LOG -ERROR_RAISE = SubRipFile.ERROR_RAISE - -open = SubRipFile.open -stream = SubRipFile.stream -from_string = SubRipFile.from_string diff --git a/lib/pysrt/commands.py b/lib/pysrt/commands.py deleted file mode 100644 index 557c663d..00000000 --- a/lib/pysrt/commands.py +++ /dev/null @@ -1,218 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# pylint: disable-all - -import os -import re -import sys -import codecs -import shutil -import argparse -from textwrap import dedent - -from chardet import detect -from pysrt import SubRipFile, SubRipTime, VERSION_STRING - -def underline(string): - return "\033[4m%s\033[0m" % string - - -class TimeAwareArgumentParser(argparse.ArgumentParser): - - RE_TIME_REPRESENTATION = re.compile(r'^\-?(\d+[hms]{0,2}){1,4}$') - - def parse_args(self, args=None, namespace=None): - time_index = -1 - for index, arg in enumerate(args): - match = self.RE_TIME_REPRESENTATION.match(arg) - if match: - time_index = index - break - - if time_index >= 0: - args.insert(time_index, '--') - - return super(TimeAwareArgumentParser, self).parse_args(args, namespace) - - -class SubRipShifter(object): - - BACKUP_EXTENSION = '.bak' - RE_TIME_STRING = re.compile(r'(\d+)([hms]{0,2})') - UNIT_RATIOS = { - 'ms': 1, - '': SubRipTime.SECONDS_RATIO, - 's': SubRipTime.SECONDS_RATIO, - 'm': SubRipTime.MINUTES_RATIO, - 'h': SubRipTime.HOURS_RATIO, - } - DESCRIPTION = dedent("""\ - Srt subtitle editor - - It can either shift, split or change the frame rate. - """) - TIMESTAMP_HELP = "A timestamp in the form: [-][Hh][Mm]S[s][MSms]" - SHIFT_EPILOG = dedent("""\ - - Examples: - 1 minute and 12 seconds foreward (in place): - $ srt -i shift 1m12s movie.srt - - half a second foreward: - $ srt shift 500ms movie.srt > othername.srt - - 1 second and half backward: - $ srt -i shift -1s500ms movie.srt - - 3 seconds backward: - $ srt -i shift -3 movie.srt - """) - RATE_EPILOG = dedent("""\ - - Examples: - Convert 23.9fps subtitles to 25fps: - $ srt -i rate 23.9 25 movie.srt - """) - LIMITS_HELP = "Each parts duration in the form: [Hh][Mm]S[s][MSms]" - SPLIT_EPILOG = dedent("""\ - - Examples: - For a movie in 2 parts with the first part 48 minutes and 18 seconds long: - $ srt split 48m18s movie.srt - => creates movie.1.srt and movie.2.srt - - For a movie in 3 parts of 20 minutes each: - $ srt split 20m 20m movie.srt - => creates movie.1.srt, movie.2.srt and movie.3.srt - """) - FRAME_RATE_HELP = "A frame rate in fps (commonly 23.9 or 25)" - ENCODING_HELP = dedent("""\ - Change file encoding. Useful for players accepting only latin1 subtitles. - List of supported encodings: http://docs.python.org/library/codecs.html#standard-encodings - """) - BREAK_EPILOG = dedent("""\ - Break lines longer than defined length - """) - LENGTH_HELP = "Maximum number of characters per line" - - def __init__(self): - self.output_file_path = None - - def build_parser(self): - parser = TimeAwareArgumentParser(description=self.DESCRIPTION, formatter_class=argparse.RawTextHelpFormatter) - parser.add_argument('-i', '--in-place', action='store_true', dest='in_place', - help="Edit file in-place, saving a backup as file.bak (do not works for the split command)") - parser.add_argument('-e', '--output-encoding', metavar=underline('encoding'), action='store', dest='output_encoding', - type=self.parse_encoding, help=self.ENCODING_HELP) - parser.add_argument('-v', '--version', action='version', version='%%(prog)s %s' % VERSION_STRING) - subparsers = parser.add_subparsers(title='commands') - - shift_parser = subparsers.add_parser('shift', help="Shift subtitles by specified time offset", epilog=self.SHIFT_EPILOG, formatter_class=argparse.RawTextHelpFormatter) - shift_parser.add_argument('time_offset', action='store', metavar=underline('offset'), - type=self.parse_time, help=self.TIMESTAMP_HELP) - shift_parser.set_defaults(action=self.shift) - - rate_parser = subparsers.add_parser('rate', help="Convert subtitles from a frame rate to another", epilog=self.RATE_EPILOG, formatter_class=argparse.RawTextHelpFormatter) - rate_parser.add_argument('initial', action='store', type=float, help=self.FRAME_RATE_HELP) - rate_parser.add_argument('final', action='store', type=float, help=self.FRAME_RATE_HELP) - rate_parser.set_defaults(action=self.rate) - - split_parser = subparsers.add_parser('split', help="Split a file in multiple parts", epilog=self.SPLIT_EPILOG, formatter_class=argparse.RawTextHelpFormatter) - split_parser.add_argument('limits', action='store', nargs='+', type=self.parse_time, help=self.LIMITS_HELP) - split_parser.set_defaults(action=self.split) - - break_parser = subparsers.add_parser('break', help="Break long lines", epilog=self.BREAK_EPILOG, formatter_class=argparse.RawTextHelpFormatter) - break_parser.add_argument('length', action='store', type=int, help=self.LENGTH_HELP) - break_parser.set_defaults(action=self.break_lines) - - parser.add_argument('file', action='store') - - return parser - - def run(self, args): - self.arguments = self.build_parser().parse_args(args) - if self.arguments.in_place: - self.create_backup() - self.arguments.action() - - def parse_time(self, time_string): - negative = time_string.startswith('-') - if negative: - time_string = time_string[1:] - ordinal = sum(int(value) * self.UNIT_RATIOS[unit] for value, unit - in self.RE_TIME_STRING.findall(time_string)) - return -ordinal if negative else ordinal - - def parse_encoding(self, encoding_name): - try: - codecs.lookup(encoding_name) - except LookupError as error: - raise argparse.ArgumentTypeError(error.message) - return encoding_name - - def shift(self): - self.input_file.shift(milliseconds=self.arguments.time_offset) - self.input_file.write_into(self.output_file) - - def rate(self): - ratio = self.arguments.final / self.arguments.initial - self.input_file.shift(ratio=ratio) - self.input_file.write_into(self.output_file) - - def split(self): - limits = [0] + self.arguments.limits + [self.input_file[-1].end.ordinal + 1] - base_name, extension = os.path.splitext(self.arguments.file) - for index, (start, end) in enumerate(zip(limits[:-1], limits[1:])): - file_name = '%s.%s%s' % (base_name, index + 1, extension) - part_file = self.input_file.slice(ends_after=start, starts_before=end) - part_file.shift(milliseconds=-start) - part_file.clean_indexes() - part_file.save(path=file_name, encoding=self.output_encoding) - - def create_backup(self): - backup_file = self.arguments.file + self.BACKUP_EXTENSION - if not os.path.exists(backup_file): - shutil.copy2(self.arguments.file, backup_file) - self.output_file_path = self.arguments.file - self.arguments.file = backup_file - - def break_lines(self): - split_re = re.compile(r'(.{,%i})(?:\s+|$)' % self.arguments.length) - for item in self.input_file: - item.text = '\n'.join(split_re.split(item.text)[1::2]) - self.input_file.write_into(self.output_file) - - @property - def output_encoding(self): - return self.arguments.output_encoding or self.input_file.encoding - - @property - def input_file(self): - if not hasattr(self, '_source_file'): - with open(self.arguments.file, 'rb') as f: - content = f.read() - encoding = detect(content).get('encoding') - encoding = self.normalize_encoding(encoding) - - self._source_file = SubRipFile.open(self.arguments.file, - encoding=encoding, error_handling=SubRipFile.ERROR_LOG) - return self._source_file - - @property - def output_file(self): - if not hasattr(self, '_output_file'): - if self.output_file_path: - self._output_file = codecs.open(self.output_file_path, 'w+', encoding=self.output_encoding) - else: - self._output_file = sys.stdout - return self._output_file - - def normalize_encoding(self, encoding): - return encoding.lower().replace('-', '_') - - -def main(): - SubRipShifter().run(sys.argv[1:]) - -if __name__ == '__main__': - main() diff --git a/lib/pysrt/comparablemixin.py b/lib/pysrt/comparablemixin.py deleted file mode 100644 index 3ae70b07..00000000 --- a/lib/pysrt/comparablemixin.py +++ /dev/null @@ -1,26 +0,0 @@ -class ComparableMixin(object): - def _compare(self, other, method): - try: - return method(self._cmpkey(), other._cmpkey()) - except (AttributeError, TypeError): - # _cmpkey not implemented, or return different type, - # so I can't compare with "other". - return NotImplemented - - def __lt__(self, other): - return self._compare(other, lambda s, o: s < o) - - def __le__(self, other): - return self._compare(other, lambda s, o: s <= o) - - def __eq__(self, other): - return self._compare(other, lambda s, o: s == o) - - def __ge__(self, other): - return self._compare(other, lambda s, o: s >= o) - - def __gt__(self, other): - return self._compare(other, lambda s, o: s > o) - - def __ne__(self, other): - return self._compare(other, lambda s, o: s != o) diff --git a/lib/pysrt/compat.py b/lib/pysrt/compat.py deleted file mode 100644 index 653cf320..00000000 --- a/lib/pysrt/compat.py +++ /dev/null @@ -1,24 +0,0 @@ - -import sys - -# Syntax sugar. -_ver = sys.version_info - -#: Python 2.x? -is_py2 = (_ver[0] == 2) - -#: Python 3.x? -is_py3 = (_ver[0] == 3) - -from io import open as io_open - -if is_py2: - builtin_str = str - basestring = basestring - str = unicode - open = io_open -elif is_py3: - builtin_str = str - basestring = (str, bytes) - str = str - open = open diff --git a/lib/pysrt/srtexc.py b/lib/pysrt/srtexc.py deleted file mode 100644 index 971b4709..00000000 --- a/lib/pysrt/srtexc.py +++ /dev/null @@ -1,31 +0,0 @@ -""" -Exception classes -""" - - -class Error(Exception): - """ - Pysrt's base exception - """ - pass - - -class InvalidTimeString(Error): - """ - Raised when parser fail on bad formated time strings - """ - pass - - -class InvalidItem(Error): - """ - Raised when parser fail to parse a sub title item - """ - pass - - -class InvalidIndex(InvalidItem): - """ - Raised when parser fail to parse a sub title index - """ - pass diff --git a/lib/pysrt/srtfile.py b/lib/pysrt/srtfile.py deleted file mode 100644 index 350e4b4d..00000000 --- a/lib/pysrt/srtfile.py +++ /dev/null @@ -1,312 +0,0 @@ -# -*- coding: utf-8 -*- -import os -import sys -import codecs - -try: - from collections import UserList -except ImportError: - from UserList import UserList - -from itertools import chain -from copy import copy - -from pysrt.srtexc import Error -from pysrt.srtitem import SubRipItem -from pysrt.compat import str - -BOMS = ((codecs.BOM_UTF32_LE, 'utf_32_le'), - (codecs.BOM_UTF32_BE, 'utf_32_be'), - (codecs.BOM_UTF16_LE, 'utf_16_le'), - (codecs.BOM_UTF16_BE, 'utf_16_be'), - (codecs.BOM_UTF8, 'utf_8')) -CODECS_BOMS = dict((codec, str(bom, codec)) for bom, codec in BOMS) -BIGGER_BOM = max(len(bom) for bom, encoding in BOMS) - - -class SubRipFile(UserList, object): - """ - SubRip file descriptor. - - Provide a pure Python mapping on all metadata. - - SubRipFile(items, eol, path, encoding) - - items -> list of SubRipItem. Default to []. - eol -> str: end of line character. Default to linesep used in opened file - if any else to os.linesep. - path -> str: path where file will be saved. To open an existant file see - SubRipFile.open. - encoding -> str: encoding used at file save. Default to utf-8. - """ - ERROR_PASS = 0 - ERROR_LOG = 1 - ERROR_RAISE = 2 - - DEFAULT_ENCODING = 'utf_8' - - def __init__(self, items=None, eol=None, path=None, encoding='utf-8'): - UserList.__init__(self, items or []) - self._eol = eol - self.path = path - self.encoding = encoding - - def _get_eol(self): - return self._eol or os.linesep - - def _set_eol(self, eol): - self._eol = self._eol or eol - - eol = property(_get_eol, _set_eol) - - def slice(self, starts_before=None, starts_after=None, ends_before=None, - ends_after=None): - """ - slice([starts_before][, starts_after][, ends_before][, ends_after]) \ --> SubRipFile clone - - All arguments are optional, and should be coercible to SubRipTime - object. - - It reduce the set of subtitles to those that match match given time - constraints. - - The returned set is a clone, but still contains references to original - subtitles. So if you shift this returned set, subs contained in the - original SubRipFile instance will be altered too. - - Example: - >>> subs.slice(ends_after={'seconds': 20}).shift(seconds=2) - """ - clone = copy(self) - - if starts_before: - clone.data = (i for i in clone.data if i.start < starts_before) - if starts_after: - clone.data = (i for i in clone.data if i.start > starts_after) - if ends_before: - clone.data = (i for i in clone.data if i.end < ends_before) - if ends_after: - clone.data = (i for i in clone.data if i.end > ends_after) - - clone.data = list(clone.data) - return clone - - def at(self, timestamp=None, **kwargs): - """ - at(timestamp) -> SubRipFile clone - - timestamp argument should be coercible to SubRipFile object. - - A specialization of slice. Return all subtiles visible at the - timestamp mark. - - Example: - >>> subs.at((0, 0, 20, 0)).shift(seconds=2) - >>> subs.at(seconds=20).shift(seconds=2) - """ - time = timestamp or kwargs - return self.slice(starts_before=time, ends_after=time) - - def shift(self, *args, **kwargs): - """shift(hours, minutes, seconds, milliseconds, ratio) - - Shift `start` and `end` attributes of each items of file either by - applying a ratio or by adding an offset. - - `ratio` should be either an int or a float. - Example to convert subtitles from 23.9 fps to 25 fps: - >>> subs.shift(ratio=25/23.9) - - All "time" arguments are optional and have a default value of 0. - Example to delay all subs from 2 seconds and half - >>> subs.shift(seconds=2, milliseconds=500) - """ - for item in self: - item.shift(*args, **kwargs) - - def clean_indexes(self): - """ - clean_indexes() - - Sort subs and reset their index attribute. Should be called after - destructive operations like split or such. - """ - self.sort() - for index, item in enumerate(self): - item.index = index + 1 - - @property - def text(self): - return '\n'.join(i.text for i in self) - - @classmethod - def open(cls, path='', encoding=None, error_handling=ERROR_PASS): - """ - open([path, [encoding]]) - - If you do not provide any encoding, it can be detected if the file - contain a bit order mark, unless it is set to utf-8 as default. - """ - new_file = cls(path=path, encoding=encoding) - source_file = cls._open_unicode_file(path, claimed_encoding=encoding) - new_file.read(source_file, error_handling=error_handling) - source_file.close() - return new_file - - @classmethod - def from_string(cls, source, **kwargs): - """ - from_string(source, **kwargs) -> SubRipFile - - `source` -> a unicode instance or at least a str instance encoded with - `sys.getdefaultencoding()` - """ - error_handling = kwargs.pop('error_handling', None) - new_file = cls(**kwargs) - new_file.read(source.splitlines(True), error_handling=error_handling) - return new_file - - def read(self, source_file, error_handling=ERROR_PASS): - """ - read(source_file, [error_handling]) - - This method parse subtitles contained in `source_file` and append them - to the current instance. - - `source_file` -> Any iterable that yield unicode strings, like a file - opened with `codecs.open()` or an array of unicode. - """ - self.eol = self._guess_eol(source_file) - self.extend(self.stream(source_file, error_handling=error_handling)) - return self - - @classmethod - def stream(cls, source_file, error_handling=ERROR_PASS): - """ - stream(source_file, [error_handling]) - - This method yield SubRipItem instances a soon as they have been parsed - without storing them. It is a kind of SAX parser for .srt files. - - `source_file` -> Any iterable that yield unicode strings, like a file - opened with `codecs.open()` or an array of unicode. - - Example: - >>> import pysrt - >>> import codecs - >>> file = codecs.open('movie.srt', encoding='utf-8') - >>> for sub in pysrt.stream(file): - ... sub.text += "\nHello !" - ... print unicode(sub) - """ - string_buffer = [] - for index, line in enumerate(chain(source_file, '\n')): - if line.strip(): - string_buffer.append(line) - else: - source = string_buffer - string_buffer = [] - if source and all(source): - try: - yield SubRipItem.from_lines(source) - except Error as error: - error.args += (''.join(source), ) - cls._handle_error(error, error_handling, index) - - def save(self, path=None, encoding=None, eol=None): - """ - save([path][, encoding][, eol]) - - Use initial path if no other provided. - Use initial encoding if no other provided. - Use initial eol if no other provided. - """ - path = path or self.path - encoding = encoding or self.encoding - - save_file = codecs.open(path, 'w+', encoding=encoding) - self.write_into(save_file, eol=eol) - save_file.close() - - def write_into(self, output_file, eol=None): - """ - write_into(output_file [, eol]) - - Serialize current state into `output_file`. - - `output_file` -> Any instance that respond to `write()`, typically a - file object - """ - output_eol = eol or self.eol - - for item in self: - string_repr = str(item) - if output_eol != '\n': - string_repr = string_repr.replace('\n', output_eol) - output_file.write(string_repr) - # Only add trailing eol if it's not already present. - # It was kept in the SubRipItem's text before but it really - # belongs here. Existing applications might give us subtitles - # which already contain a trailing eol though. - if not string_repr.endswith(2 * output_eol): - output_file.write(output_eol) - - @classmethod - def _guess_eol(cls, string_iterable): - first_line = cls._get_first_line(string_iterable) - for eol in ('\r\n', '\r', '\n'): - if first_line.endswith(eol): - return eol - return os.linesep - - @classmethod - def _get_first_line(cls, string_iterable): - if hasattr(string_iterable, 'tell'): - previous_position = string_iterable.tell() - - try: - first_line = next(iter(string_iterable)) - except StopIteration: - return '' - if hasattr(string_iterable, 'seek'): - string_iterable.seek(previous_position) - - return first_line - - @classmethod - def _detect_encoding(cls, path): - file_descriptor = open(path, 'rb') - first_chars = file_descriptor.read(BIGGER_BOM) - file_descriptor.close() - - for bom, encoding in BOMS: - if first_chars.startswith(bom): - return encoding - - # TODO: maybe a chardet integration - return cls.DEFAULT_ENCODING - - @classmethod - def _open_unicode_file(cls, path, claimed_encoding=None): - encoding = claimed_encoding or cls._detect_encoding(path) - source_file = codecs.open(path, 'rU', encoding=encoding) - - # get rid of BOM if any - possible_bom = CODECS_BOMS.get(encoding, None) - if possible_bom: - file_bom = source_file.read(len(possible_bom)) - if not file_bom == possible_bom: - source_file.seek(0) # if not rewind - return source_file - - @classmethod - def _handle_error(cls, error, error_handling, index): - if error_handling == cls.ERROR_RAISE: - error.args = (index, ) + error.args - raise error - if error_handling == cls.ERROR_LOG: - name = type(error).__name__ - sys.stderr.write('PySRT-%s(line %s): \n' % (name, index)) - sys.stderr.write(error.args[0].encode('ascii', 'replace')) - sys.stderr.write('\n') diff --git a/lib/pysrt/srtitem.py b/lib/pysrt/srtitem.py deleted file mode 100644 index 4101716b..00000000 --- a/lib/pysrt/srtitem.py +++ /dev/null @@ -1,76 +0,0 @@ -# -*- coding: utf-8 -*- -""" -SubRip's subtitle parser -""" -from pysrt.srtexc import InvalidItem, InvalidIndex -from pysrt.srttime import SubRipTime -from pysrt.comparablemixin import ComparableMixin -from pysrt.compat import str - -class SubRipItem(ComparableMixin): - """ - SubRipItem(index, start, end, text, position) - - index -> int: index of item in file. 0 by default. - start, end -> SubRipTime or coercible. - text -> unicode: text content for item. - position -> unicode: raw srt/vtt "display coordinates" string - """ - ITEM_PATTERN = '%s\n%s --> %s%s\n%s\n' - TIMESTAMP_SEPARATOR = '-->' - - def __init__(self, index=0, start=None, end=None, text='', position=''): - try: - self.index = int(index) - except (TypeError, ValueError): # try to cast as int, but it's not mandatory - self.index = index - - self.start = SubRipTime.coerce(start or 0) - self.end = SubRipTime.coerce(end or 0) - self.position = str(position) - self.text = str(text) - - def __str__(self): - position = ' %s' % self.position if self.position.strip() else '' - return self.ITEM_PATTERN % (self.index, self.start, self.end, - position, self.text) - - def _cmpkey(self): - return (self.start, self.end) - - def shift(self, *args, **kwargs): - """ - shift(hours, minutes, seconds, milliseconds, ratio) - - Add given values to start and end attributes. - All arguments are optional and have a default value of 0. - """ - self.start.shift(*args, **kwargs) - self.end.shift(*args, **kwargs) - - @classmethod - def from_string(cls, source): - return cls.from_lines(source.splitlines(True)) - - @classmethod - def from_lines(cls, lines): - if len(lines) < 2: - raise InvalidItem() - lines = [l.rstrip() for l in lines] - index = None - if cls.TIMESTAMP_SEPARATOR not in lines[0]: - index = lines.pop(0) - start, end, position = cls.split_timestamps(lines[0]) - body = '\n'.join(lines[1:]) - return cls(index, start, end, body, position) - - @classmethod - def split_timestamps(cls, line): - timestamps = line.split(cls.TIMESTAMP_SEPARATOR) - if len(timestamps) != 2: - raise InvalidItem() - start, end_and_position = timestamps - end_and_position = end_and_position.lstrip().split(' ', 1) - end = end_and_position[0] - position = end_and_position[1] if len(end_and_position) > 1 else '' - return (s.strip() for s in (start, end, position)) diff --git a/lib/pysrt/srttime.py b/lib/pysrt/srttime.py deleted file mode 100644 index 95c578f8..00000000 --- a/lib/pysrt/srttime.py +++ /dev/null @@ -1,176 +0,0 @@ -# -*- coding: utf-8 -*- -""" -SubRip's time format parser: HH:MM:SS,mmm -""" -import re -from datetime import time - -from pysrt.srtexc import InvalidTimeString -from pysrt.comparablemixin import ComparableMixin -from pysrt.compat import str, basestring - -class TimeItemDescriptor(object): - # pylint: disable-msg=R0903 - def __init__(self, ratio, super_ratio=0): - self.ratio = int(ratio) - self.super_ratio = int(super_ratio) - - def _get_ordinal(self, instance): - if self.super_ratio: - return instance.ordinal % self.super_ratio - return instance.ordinal - - def __get__(self, instance, klass): - if instance is None: - raise AttributeError - return self._get_ordinal(instance) // self.ratio - - def __set__(self, instance, value): - part = self._get_ordinal(instance) - instance.ordinal % self.ratio - instance.ordinal += value * self.ratio - part - - -class SubRipTime(ComparableMixin): - TIME_PATTERN = '%02d:%02d:%02d,%03d' - TIME_REPR = 'SubRipTime(%d, %d, %d, %d)' - RE_TIME_SEP = re.compile(r'\:|\.|\,') - RE_INTEGER = re.compile(r'^(\d+)') - SECONDS_RATIO = 1000 - MINUTES_RATIO = SECONDS_RATIO * 60 - HOURS_RATIO = MINUTES_RATIO * 60 - - hours = TimeItemDescriptor(HOURS_RATIO) - minutes = TimeItemDescriptor(MINUTES_RATIO, HOURS_RATIO) - seconds = TimeItemDescriptor(SECONDS_RATIO, MINUTES_RATIO) - milliseconds = TimeItemDescriptor(1, SECONDS_RATIO) - - def __init__(self, hours=0, minutes=0, seconds=0, milliseconds=0): - """ - SubRipTime(hours, minutes, seconds, milliseconds) - - All arguments are optional and have a default value of 0. - """ - super(SubRipTime, self).__init__() - self.ordinal = hours * self.HOURS_RATIO \ - + minutes * self.MINUTES_RATIO \ - + seconds * self.SECONDS_RATIO \ - + milliseconds - - def __repr__(self): - return self.TIME_REPR % tuple(self) - - def __str__(self): - if self.ordinal < 0: - # Represent negative times as zero - return str(SubRipTime.from_ordinal(0)) - return self.TIME_PATTERN % tuple(self) - - def _compare(self, other, method): - return super(SubRipTime, self)._compare(self.coerce(other), method) - - def _cmpkey(self): - return self.ordinal - - def __add__(self, other): - return self.from_ordinal(self.ordinal + self.coerce(other).ordinal) - - def __iadd__(self, other): - self.ordinal += self.coerce(other).ordinal - return self - - def __sub__(self, other): - return self.from_ordinal(self.ordinal - self.coerce(other).ordinal) - - def __isub__(self, other): - self.ordinal -= self.coerce(other).ordinal - return self - - def __mul__(self, ratio): - return self.from_ordinal(int(round(self.ordinal * ratio))) - - def __imul__(self, ratio): - self.ordinal = int(round(self.ordinal * ratio)) - return self - - @classmethod - def coerce(cls, other): - """ - Coerce many types to SubRipTime instance. - Supported types: - - str/unicode - - int/long - - datetime.time - - any iterable - - dict - """ - if isinstance(other, SubRipTime): - return other - if isinstance(other, basestring): - return cls.from_string(other) - if isinstance(other, int): - return cls.from_ordinal(other) - if isinstance(other, time): - return cls.from_time(other) - try: - return cls(**other) - except TypeError: - return cls(*other) - - def __iter__(self): - yield self.hours - yield self.minutes - yield self.seconds - yield self.milliseconds - - def shift(self, *args, **kwargs): - """ - shift(hours, minutes, seconds, milliseconds) - - All arguments are optional and have a default value of 0. - """ - if 'ratio' in kwargs: - self *= kwargs.pop('ratio') - self += self.__class__(*args, **kwargs) - - @classmethod - def from_ordinal(cls, ordinal): - """ - int -> SubRipTime corresponding to a total count of milliseconds - """ - return cls(milliseconds=int(ordinal)) - - @classmethod - def from_string(cls, source): - """ - str/unicode(HH:MM:SS,mmm) -> SubRipTime corresponding to serial - raise InvalidTimeString - """ - items = cls.RE_TIME_SEP.split(source) - if len(items) != 4: - raise InvalidTimeString - return cls(*(cls.parse_int(i) for i in items)) - - @classmethod - def parse_int(cls, digits): - try: - return int(digits) - except ValueError: - match = cls.RE_INTEGER.match(digits) - if match: - return int(match.group()) - return 0 - - @classmethod - def from_time(cls, source): - """ - datetime.time -> SubRipTime corresponding to time object - """ - return cls(hours=source.hour, minutes=source.minute, - seconds=source.second, milliseconds=source.microsecond // 1000) - - def to_time(self): - """ - Convert SubRipTime instance into a pure datetime.time object - """ - return time(self.hours, self.minutes, self.seconds, - self.milliseconds * 1000) diff --git a/lib/pysrt/version.py b/lib/pysrt/version.py deleted file mode 100644 index f04e34e8..00000000 --- a/lib/pysrt/version.py +++ /dev/null @@ -1,2 +0,0 @@ -VERSION = (1, 0, 1) -VERSION_STRING = '.'.join(str(i) for i in VERSION) From 71d7938fac869225c4b321979df4e11e1de112a1 Mon Sep 17 00:00:00 2001 From: Adam Date: Mon, 23 Feb 2015 10:19:25 +0800 Subject: [PATCH 04/78] Change webserve code to a logical layout and PEP8 --- CHANGES.md | 1 + sickbeard/browser.py | 17 +- sickbeard/webapi.py | 34 - sickbeard/webserve.py | 4635 ++++++++++++++++++------------------- sickbeard/webserveInit.py | 7 +- 5 files changed, 2305 insertions(+), 2389 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0f1d30df..92b6526f 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,7 @@ * Add requirements file for pip (port from midgetspy/sick-beard) * Remove unused libraries fuzzywuzzy and pysrt +* Change webserve code to a logical layout and PEP8 [develop changelog] diff --git a/sickbeard/browser.py b/sickbeard/browser.py index df027315..5b6e6429 100644 --- a/sickbeard/browser.py +++ b/sickbeard/browser.py @@ -19,10 +19,8 @@ import os import string -from tornado.httputil import HTTPHeaders -from tornado.web import RequestHandler from sickbeard import encodingKludge as ek -from sickbeard import logger, webserve +from sickbeard import logger # use the built-in if it's available (python 2.6), if not use the included library try: @@ -104,15 +102,4 @@ def foldersAtPath(path, includeParent=False, includeFiles=False): entries.append({'name': "..", 'path': parentPath}) entries.extend(fileList) - return entries - - -class WebFileBrowser(webserve.MainHandler): - def index(self, path='', includeFiles=False, *args, **kwargs): - self.set_header("Content-Type", "application/json") - return json.dumps(foldersAtPath(path, True, bool(int(includeFiles)))) - - def complete(self, term, includeFiles=0): - self.set_header("Content-Type", "application/json") - paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term), includeFiles=bool(int(includeFiles))) if 'path' in entry] - return json.dumps(paths) \ No newline at end of file + return entries \ No newline at end of file diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py index 7770dc95..7fa3e3ef 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/webapi.py @@ -166,40 +166,6 @@ class Api(webserve.BaseHandler): return False, msg, args, kwargs -class ApiBuilder(webserve.MainHandler): - def index(self): - """ expose the api-builder template """ - t = webserve.PageTemplate(headers=self.request.headers, file="apiBuilder.tmpl") - - def titler(x): - return (remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] - - t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) - - seasonSQLResults = {} - episodeSQLResults = {} - - myDB = db.DBConnection(row_type="dict") - for curShow in t.sortedShowList: - seasonSQLResults[curShow.indexerid] = myDB.select( - "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season DESC", [curShow.indexerid]) - - for curShow in t.sortedShowList: - episodeSQLResults[curShow.indexerid] = myDB.select( - "SELECT DISTINCT season,episode FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", - [curShow.indexerid]) - - t.seasonSQLResults = seasonSQLResults - t.episodeSQLResults = episodeSQLResults - - if len(sickbeard.API_KEY) == 32: - t.apikey = sickbeard.API_KEY - else: - t.apikey = "api key not generated" - - return webserve._munge(t) - - def call_dispatcher(handler, args, kwargs): """ calls the appropriate CMD class looks for a cmd in args and kwargs diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 82c05862..a275b39a 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -18,35 +18,22 @@ from __future__ import with_statement -import base64 -import inspect -import traceback - import os - import time import urllib import re import datetime import random -import sys +import traceback + +from mimetypes import MimeTypes + +from Cheetah.Template import Template import sickbeard - -from sickbeard import config, sab -from sickbeard import clients -from sickbeard import history, notifiers, processTV -from sickbeard import ui -from sickbeard import logger, helpers, exceptions, classes, db +from sickbeard import config, sab, clients, history, notifiers, processTV, ui, logger, helpers, exceptions, classes, \ + db, search_queue, image_cache, naming, scene_exceptions, subtitles, network_timezones, sbdatetime from sickbeard import encodingKludge as ek -from sickbeard import search_queue -from sickbeard import image_cache -from sickbeard import naming -from sickbeard import scene_exceptions -from sickbeard import subtitles -from sickbeard import network_timezones -from sickbeard import sbdatetime - from sickbeard.providers import newznab, rsstorrent from sickbeard.common import Quality, Overview, statusStrings, qualityPresetStrings, cpu_presets from sickbeard.common import SNATCHED, UNAIRED, IGNORED, ARCHIVED, WANTED, FAILED @@ -57,15 +44,14 @@ from sickbeard.scene_exceptions import get_scene_exceptions from sickbeard.scene_numbering import get_scene_numbering, set_scene_numbering, get_scene_numbering_for_show, \ get_xem_numbering_for_show, get_scene_absolute_numbering_for_show, get_xem_absolute_numbering_for_show, \ get_scene_absolute_numbering - +from sickbeard.browser import foldersAtPath from sickbeard.blackandwhitelist import BlackAndWhiteList, short_group_names - -from mimetypes import MimeTypes - +from tornado import gen +from tornado.web import RequestHandler, authenticated +from lib import adba +from lib import subliminal from lib.dateutil import tz from lib.unrar2 import RarFile - -from lib import subliminal from lib.trakt import TraktCall try: @@ -73,35 +59,54 @@ try: except ImportError: from lib import simplejson as json -try: - import xml.etree.cElementTree as etree -except ImportError: - import xml.etree.ElementTree as etree -from lib import adba +class PageTemplate(Template): + def __init__(self, headers, *args, **KWs): + KWs['file'] = os.path.join(sickbeard.PROG_DIR, 'gui/' + sickbeard.GUI_NAME + '/interfaces/default/', + KWs['file']) + super(PageTemplate, self).__init__(*args, **KWs) -from Cheetah.Template import Template -from tornado.web import RequestHandler, HTTPError, asynchronous, authenticated -from tornado import gen, escape + self.sbRoot = sickbeard.WEB_ROOT + self.sbHttpPort = sickbeard.WEB_PORT + self.sbHttpsPort = sickbeard.WEB_PORT + self.sbHttpsEnabled = sickbeard.ENABLE_HTTPS + self.sbHandleReverseProxy = sickbeard.HANDLE_REVERSE_PROXY + self.sbThemeName = sickbeard.THEME_NAME + if headers['Host'][0] == '[': + self.sbHost = re.match('^\[.*\]', headers['Host'], re.X | re.M | re.S).group(0) + else: + self.sbHost = re.match('^[^:]+', headers['Host'], re.X | re.M | re.S).group(0) -class HTTPRedirect(Exception): - """Exception raised when the request should be redirected.""" + if 'X-Forwarded-Host' in headers: + self.sbHost = headers['X-Forwarded-Host'] + if 'X-Forwarded-Port' in headers: + sbHttpPort = headers['X-Forwarded-Port'] + self.sbHttpsPort = sbHttpPort + if 'X-Forwarded-Proto' in headers: + self.sbHttpsEnabled = True if headers['X-Forwarded-Proto'] == 'https' else False - def __init__(self, url, permanent=False, status=None): - self.url = url - self.permanent = permanent - self.status = status - Exception.__init__(self, self.url, self.permanent, self.status) + logPageTitle = 'Logs & Errors' + if len(classes.ErrorViewer.errors): + logPageTitle += ' (' + str(len(classes.ErrorViewer.errors)) + ')' + self.logPageTitle = logPageTitle + self.sbPID = str(sickbeard.PID) + self.menu = [ + {'title': 'Home', 'key': 'home'}, + {'title': 'Episodes', 'key': 'episodeView'}, + {'title': 'History', 'key': 'history'}, + {'title': 'Manage', 'key': 'manage'}, + {'title': 'Config', 'key': 'config'}, + {'title': logPageTitle, 'key': 'errorlogs'}, + ] - def __call__(self): - """Use this exception as a request.handler (raise self).""" - raise self + def compile(self, *args, **kwargs): + if not os.path.exists(os.path.join(sickbeard.CACHE_DIR, 'cheetah')): + os.mkdir(os.path.join(sickbeard.CACHE_DIR, 'cheetah')) - -def redirect(url, permanent=False, status=None): - assert url[0] == '/' - raise HTTPRedirect(sickbeard.WEB_ROOT + url, permanent, status) + kwargs['cacheModuleFilesForTracebacks'] = True + kwargs['cacheDirForModuleFiles'] = os.path.join(sickbeard.CACHE_DIR, 'cheetah') + return super(PageTemplate, self).compile(*args, **kwargs) class BaseHandler(RequestHandler): @@ -152,13 +157,7 @@ class BaseHandler(RequestHandler): else: static_image_path = os.path.normpath(static_image_path.replace(sickbeard.CACHE_DIR, '/cache')) static_image_path = static_image_path.replace('\\', '/') - return redirect(static_image_path) - - -class LogoutHandler(BaseHandler): - def get(self, *args, **kwargs): - self.clear_cookie('user') - self.redirect('/login/') + self.redirect(static_image_path) class LoginHandler(BaseHandler): @@ -187,10 +186,98 @@ class LoginHandler(BaseHandler): self.redirect('/login?resp=authfailed' + next_arg) +class LogoutHandler(BaseHandler): + def get(self, *args, **kwargs): + self.clear_cookie('user') + self.redirect('/login/') + + +class CalendarHandler(BaseHandler): + def get(self, *args, **kwargs): + if sickbeard.CALENDAR_UNPROTECTED or self.get_current_user(): + self.write(self.calendar()) + else: + self.set_status(401) + self.write('User authentication required') + + def calendar(self, *args, **kwargs): + """ iCalendar (iCal) - Standard RFC 5545 + Works with iCloud, Google Calendar and Outlook. + Provides a subscribeable URL for iCal subscriptions """ + + logger.log(u'Receiving iCal request from %s' % self.request.remote_ip) + + # Limit dates + past_date = (datetime.date.today() + datetime.timedelta(weeks=-52)).toordinal() + future_date = (datetime.date.today() + datetime.timedelta(weeks=52)).toordinal() + utc = tz.gettz('GMT') + + # Get all the shows that are not paused and are currently on air + myDB = db.DBConnection() + show_list = myDB.select( + 'SELECT show_name, indexer_id, network, airs, runtime FROM tv_shows WHERE ( status = "Continuing" OR status = "Returning Series" ) AND paused != "1"') + + nl = '\\n\\n' + crlf = '\r\n' + + # Create iCal header + appname = 'SickGear' + ical = 'BEGIN:VCALENDAR%sVERSION:2.0%sX-WR-CALNAME:%s%sX-WR-CALDESC:%s%sPRODID://%s Upcoming Episodes//%s'\ + % (crlf, crlf, appname, crlf, appname, crlf, appname, crlf) + + for show in show_list: + # Get all episodes of this show airing between today and next month + episode_list = myDB.select( + 'SELECT indexerid, name, season, episode, description, airdate FROM tv_episodes WHERE airdate >= ? AND airdate < ? AND showid = ?', + (past_date, future_date, int(show['indexer_id']))) + + for episode in episode_list: + + air_date_time = network_timezones.parse_date_time(episode['airdate'], show['airs'], + show['network']).astimezone(utc) + air_date_time_end = air_date_time + datetime.timedelta( + minutes=helpers.tryInt(show['runtime'], 60)) + + # Create event for episode + ical += 'BEGIN:VEVENT%s' % crlf\ + + 'DTSTART:%sT%sZ%s' % (air_date_time.strftime('%Y%m%d'), air_date_time.strftime('%H%M%S'), crlf)\ + + 'DTEND:%sT%sZ%s' % (air_date_time_end.strftime('%Y%m%d'), air_date_time_end.strftime('%H%M%S'), crlf)\ + + 'SUMMARY:%s - %sx%s - %s%s' % (show['show_name'], str(episode['season']), str(episode['episode']), episode['name'], crlf)\ + + 'UID:%s-%s-%s-E%sS%s%s' % (appname, str(datetime.date.today().isoformat()), show['show_name'].replace(' ', '-'), str(episode['episode']), str(episode['season']), crlf)\ + + 'DESCRIPTION:%s on %s' % ((show['airs'] or '(Unknown airs)'), (show['network'] or 'Unknown network'))\ + + ('' if not episode['description'] else '%s%s' % (nl, episode['description'].splitlines()[0]))\ + + '%sEND:VEVENT%s' % (crlf, crlf) + + # Ending the iCal + return ical + 'END:VCALENDAR' + + +class IsAliveHandler(BaseHandler): + def get(self, *args, **kwargs): + kwargs = self.request.arguments + if 'callback' in kwargs and '_' in kwargs: + callback, _ = kwargs['callback'][0], kwargs['_'] + else: + return 'Error: Unsupported Request. Send jsonp request with callback variable in the query string.' + + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'text/javascript') + self.set_header('Access-Control-Allow-Origin', '*') + self.set_header('Access-Control-Allow-Headers', 'x-requested-with') + + if sickbeard.started: + results = callback + '(' + json.dumps( + {'msg': str(sickbeard.PID)}) + ');' + else: + results = callback + '(' + json.dumps({'msg': 'nope'}) + ');' + + self.write(results) + + class WebHandler(BaseHandler): def page_not_found(self): t = PageTemplate(headers=self.request.headers, file='404.tmpl') - return _munge(t) + return t.respond() @authenticated @gen.coroutine @@ -205,17 +292,16 @@ class WebHandler(BaseHandler): for arg, value in kwargss.items(): if len(value) == 1: kwargss[arg] = value[0] - try: - self.finish(method(**kwargss)) - except HTTPRedirect, e: - self.redirect(e.url, e.permanent, e.status) + result = method(**kwargss) + if result: + self.finish(result) post = get class MainHandler(WebHandler): def index(self): - return redirect('/home/') + self.redirect('/home/') def http_error_401_handler(self): """ Custom handler for 401 error """ @@ -236,15 +322,15 @@ class MainHandler(WebHandler): self.finish(self.http_error_401_handler()) elif status_code == 404: self.redirect(sickbeard.WEB_ROOT + '/home/') - elif self.settings.get("debug") and "exc_info" in kwargs: - exc_info = kwargs["exc_info"] - trace_info = ''.join(["%s
" % line for line in traceback.format_exception(*exc_info)]) - request_info = ''.join(["%s: %s
" % (k, self.request.__dict__[k] ) for k in + elif self.settings.get('debug') and 'exc_info' in kwargs: + exc_info = kwargs['exc_info'] + trace_info = ''.join(['%s
' % line for line in traceback.format_exception(*exc_info)]) + request_info = ''.join(['%s: %s
' % (k, self.request.__dict__[k] ) for k in self.request.__dict__.keys()]) error = exc_info[1] self.set_header('Content-Type', 'text/html') - self.finish(""" + self.finish(''' %s

Error

@@ -254,13 +340,13 @@ class MainHandler(WebHandler):

Request Info

%s

- """ % (error, error, + ''' % (error, error, trace_info, request_info)) def robots_txt(self, *args, **kwargs): """ Keep web crawlers out """ self.set_header('Content-Type', 'text/plain') - return "User-agent: *\nDisallow: /" + return 'User-agent: *\nDisallow: /' def setHomeLayout(self, layout): @@ -269,7 +355,7 @@ class MainHandler(WebHandler): sickbeard.HOME_LAYOUT = layout - redirect("/home/") + self.redirect('/home/') def setPosterSortBy(self, sort): @@ -291,13 +377,13 @@ class MainHandler(WebHandler): sickbeard.HISTORY_LAYOUT = layout - redirect("/history/") + self.redirect('/history/') def toggleDisplayShowSpecials(self, show): sickbeard.DISPLAY_SHOW_SPECIALS = not sickbeard.DISPLAY_SHOW_SPECIALS - redirect("/home/displayShow?show=" + show) + self.redirect('/home/displayShow?show=' + show) def setEpisodeViewLayout(self, layout): if layout not in ('poster', 'banner', 'list', 'daybyday'): @@ -310,7 +396,7 @@ class MainHandler(WebHandler): sickbeard.save_config() - redirect("/episodeView/") + self.redirect('/episodeView/') def toggleEpisodeViewDisplayPaused(self, *args, **kwargs): @@ -318,7 +404,7 @@ class MainHandler(WebHandler): sickbeard.save_config() - redirect("/episodeView/") + self.redirect('/episodeView/') def setEpisodeViewSort(self, sort, redir=1): if sort not in ('time', 'network', 'show'): @@ -329,9 +415,9 @@ class MainHandler(WebHandler): sickbeard.save_config() if int(redir): - redirect("/episodeView/") + self.redirect('/episodeView/') - def episodeView(self, layout="None"): + def episodeView(self, layout='None'): """ display the episodes """ today_dt = datetime.date.today() #today = today_dt.toordinal() @@ -409,270 +495,1861 @@ class MainHandler(WebHandler): else: t.layout = sickbeard.EPISODE_VIEW_LAYOUT - return _munge(t) + return t.respond() def _genericMessage(self, subject, message): - t = PageTemplate(headers=self.request.headers, file="genericMessage.tmpl") - t.submenu = HomeMenu() + t = PageTemplate(headers=self.request.headers, file='genericMessage.tmpl') + t.submenu = self.HomeMenu() t.subject = subject t.message = message - return _munge(t) + return t.respond() -class CalendarHandler(BaseHandler): - def get(self, *args, **kwargs): - if sickbeard.CALENDAR_UNPROTECTED or self.get_current_user(): - self.write(self.calendar()) - else: - self.set_status(401) - self.write('User authentication required') - - def calendar(self, *args, **kwargs): - """ iCalendar (iCal) - Standard RFC 5545 - Works with iCloud, Google Calendar and Outlook. - Provides a subscribeable URL for iCal subscriptions """ - - logger.log(u'Receiving iCal request from %s' % self.request.remote_ip) - - # Limit dates - past_date = (datetime.date.today() + datetime.timedelta(weeks=-52)).toordinal() - future_date = (datetime.date.today() + datetime.timedelta(weeks=52)).toordinal() - utc = tz.gettz('GMT') - - # Get all the shows that are not paused and are currently on air - myDB = db.DBConnection() - show_list = myDB.select( - 'SELECT show_name, indexer_id, network, airs, runtime FROM tv_shows WHERE ( status = "Continuing" OR status = "Returning Series" ) AND paused != "1"') - - nl = "\\n\\n" - crlf = "\r\n" - - # Create iCal header - appname = 'SickGear' - ical = 'BEGIN:VCALENDAR%sVERSION:2.0%sX-WR-CALNAME:%s%sX-WR-CALDESC:%s%sPRODID://%s Upcoming Episodes//%s'\ - % (crlf, crlf, appname, crlf, appname, crlf, appname, crlf) - - for show in show_list: - # Get all episodes of this show airing between today and next month - episode_list = myDB.select( - 'SELECT indexerid, name, season, episode, description, airdate FROM tv_episodes WHERE airdate >= ? AND airdate < ? AND showid = ?', - (past_date, future_date, int(show['indexer_id']))) - - for episode in episode_list: - - air_date_time = network_timezones.parse_date_time(episode['airdate'], show['airs'], - show['network']).astimezone(utc) - air_date_time_end = air_date_time + datetime.timedelta( - minutes=helpers.tryInt(show['runtime'], 60)) - - # Create event for episode - ical += 'BEGIN:VEVENT%s' % crlf\ - + 'DTSTART:%sT%sZ%s' % (air_date_time.strftime('%Y%m%d'), air_date_time.strftime('%H%M%S'), crlf)\ - + 'DTEND:%sT%sZ%s' % (air_date_time_end.strftime('%Y%m%d'), air_date_time_end.strftime('%H%M%S'), crlf)\ - + 'SUMMARY:%s - %sx%s - %s%s' % (show['show_name'], str(episode['season']), str(episode['episode']), episode['name'], crlf)\ - + 'UID:%s-%s-%s-E%sS%s%s' % (appname, str(datetime.date.today().isoformat()), show['show_name'].replace(' ', '-'), str(episode['episode']), str(episode['season']), crlf)\ - + 'DESCRIPTION:%s on %s' % ((show['airs'] or '(Unknown airs)'), (show['network'] or 'Unknown network'))\ - + ('' if not episode['description'] else '%s%s' % (nl, episode['description'].splitlines()[0]))\ - + '%sEND:VEVENT%s' % (crlf, crlf) - - # Ending the iCal - return ical + 'END:VCALENDAR' - - -class IsAliveHandler(BaseHandler): - def get(self, *args, **kwargs): - kwargs = self.request.arguments - if 'callback' in kwargs and '_' in kwargs: - callback, _ = kwargs['callback'][0], kwargs['_'] - else: - return "Error: Unsupported Request. Send jsonp request with 'callback' variable in the query string." - - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - self.set_header('Content-Type', 'text/javascript') - self.set_header('Access-Control-Allow-Origin', '*') - self.set_header('Access-Control-Allow-Headers', 'x-requested-with') - - if sickbeard.started: - results = callback + '(' + json.dumps( - {"msg": str(sickbeard.PID)}) + ');' - else: - results = callback + '(' + json.dumps({"msg": "nope"}) + ');' - - self.write(results) - - -class PageTemplate(Template): - def __init__(self, headers, *args, **KWs): - KWs['file'] = os.path.join(sickbeard.PROG_DIR, "gui/" + sickbeard.GUI_NAME + "/interfaces/default/", - KWs['file']) - super(PageTemplate, self).__init__(*args, **KWs) - - self.sbRoot = sickbeard.WEB_ROOT - self.sbHttpPort = sickbeard.WEB_PORT - self.sbHttpsPort = sickbeard.WEB_PORT - self.sbHttpsEnabled = sickbeard.ENABLE_HTTPS - self.sbHandleReverseProxy = sickbeard.HANDLE_REVERSE_PROXY - self.sbThemeName = sickbeard.THEME_NAME - - if headers['Host'][0] == '[': - self.sbHost = re.match("^\[.*\]", headers['Host'], re.X | re.M | re.S).group(0) - else: - self.sbHost = re.match("^[^:]+", headers['Host'], re.X | re.M | re.S).group(0) - - if "X-Forwarded-Host" in headers: - self.sbHost = headers['X-Forwarded-Host'] - if "X-Forwarded-Port" in headers: - sbHttpPort = headers['X-Forwarded-Port'] - self.sbHttpsPort = sbHttpPort - if "X-Forwarded-Proto" in headers: - self.sbHttpsEnabled = True if headers['X-Forwarded-Proto'] == 'https' else False - - logPageTitle = 'Logs & Errors' - if len(classes.ErrorViewer.errors): - logPageTitle += ' (' + str(len(classes.ErrorViewer.errors)) + ')' - self.logPageTitle = logPageTitle - self.sbPID = str(sickbeard.PID) - self.menu = [ - {'title': 'Home', 'key': 'home'}, - {'title': 'Episodes', 'key': 'episodeView'}, - {'title': 'History', 'key': 'history'}, - {'title': 'Manage', 'key': 'manage'}, - {'title': 'Config', 'key': 'config'}, - {'title': logPageTitle, 'key': 'errorlogs'}, +class Home(MainHandler): + def HomeMenu(self): + return [ + {'title': 'Add Shows', 'path': 'home/addShows/', }, + {'title': 'Manual Post-Processing', 'path': 'home/postprocess/'}, + {'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': self.haveXBMC}, + {'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': self.havePLEX}, + {'title': 'Manage Torrents', 'path': 'manage/manageTorrents', 'requires': self.haveTORRENT}, + {'title': 'Restart', 'path': 'home/restart/?pid=' + str(sickbeard.PID), 'confirm': True}, + {'title': 'Shutdown', 'path': 'home/shutdown/?pid=' + str(sickbeard.PID), 'confirm': True}, ] - def compile(self, *args, **kwargs): - if not os.path.exists(os.path.join(sickbeard.CACHE_DIR, 'cheetah')): - os.mkdir(os.path.join(sickbeard.CACHE_DIR, 'cheetah')) + @staticmethod + def haveXBMC(): + return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY - kwargs['cacheModuleFilesForTracebacks'] = True - kwargs['cacheDirForModuleFiles'] = os.path.join(sickbeard.CACHE_DIR, 'cheetah') - return super(PageTemplate, self).compile(*args, **kwargs) + @staticmethod + def havePLEX(): + return sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY - -class IndexerWebUI(MainHandler): - def __init__(self, config, log=None): - self.config = config - self.log = log - - def selectSeries(self, allSeries): - searchList = ",".join([x['id'] for x in allSeries]) - showDirList = "" - for curShowDir in self.config['_showDir']: - showDirList += "showDir=" + curShowDir + "&" - redirect("/home/addShows/addShow?" + showDirList + "seriesList=" + searchList) - - -def _munge(string): - return unicode(string).encode('utf-8', 'xmlcharrefreplace') - - -def _getEpisode(show, season=None, episode=None, absolute=None): - if show is None: - return "Invalid show parameters" - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return "Invalid show paramaters" - - if absolute: - epObj = showObj.getEpisode(absolute_number=int(absolute)) - elif season and episode: - epObj = showObj.getEpisode(int(season), int(episode)) - else: - return "Invalid paramaters" - - if epObj is None: - return "Episode couldn't be retrieved" - - return epObj - - -def ManageMenu(): - manageMenu = [ - {'title': 'Backlog Overview', 'path': 'manage/backlogOverview/'}, - {'title': 'Manage Searches', 'path': 'manage/manageSearches/'}, - {'title': 'Episode Status Management', 'path': 'manage/episodeStatuses/'}, ] - - if sickbeard.USE_TORRENTS and sickbeard.TORRENT_METHOD != 'blackhole' \ - and (sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'https' - or not sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'http:'): - manageMenu.append({'title': 'Manage Torrents', 'path': 'manage/manageTorrents/'}) - - if sickbeard.USE_SUBTITLES: - manageMenu.append({'title': 'Missed Subtitle Management', 'path': 'manage/subtitleMissed/'}) - - if sickbeard.USE_FAILED_DOWNLOADS: - manageMenu.append({'title': 'Failed Downloads', 'path': 'manage/failedDownloads/'}) - - return manageMenu - - -class ManageSearches(MainHandler): - def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="manage_manageSearches.tmpl") - # t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() - t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() # @UndefinedVariable - t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() # @UndefinedVariable - t.recentSearchStatus = sickbeard.recentSearchScheduler.action.amActive # @UndefinedVariable - t.findPropersStatus = sickbeard.properFinderScheduler.action.amActive # @UndefinedVariable - t.queueLength = sickbeard.searchQueueScheduler.action.queue_length() - - t.submenu = ManageMenu() - - return _munge(t) - - def forceVersionCheck(self, *args, **kwargs): - # force a check to see if there is a new version - if sickbeard.versionCheckScheduler.action.check_for_new_version(force=True): - logger.log(u"Forcing version check") - - redirect("/home/") - - def forceBacklog(self, *args, **kwargs): - # force it to run the next time it looks - result = sickbeard.backlogSearchScheduler.forceRun() - if result: - logger.log(u"Backlog search forced") - ui.notifications.message('Backlog search started') - - redirect("/manage/manageSearches/") - - def forceSearch(self, *args, **kwargs): - - # force it to run the next time it looks - result = sickbeard.recentSearchScheduler.forceRun() - if result: - logger.log(u"Recent search forced") - ui.notifications.message('Recent search started') - - redirect("/manage/manageSearches/") - - def forceFindPropers(self, *args, **kwargs): - - # force it to run the next time it looks - result = sickbeard.properFinderScheduler.forceRun() - if result: - logger.log(u"Find propers search forced") - ui.notifications.message('Find propers search started') - - redirect("/manage/manageSearches/") - - def pauseBacklog(self, paused=None): - if paused == "1": - sickbeard.searchQueueScheduler.action.pause_backlog() # @UndefinedVariable + @staticmethod + def haveTORRENT(): + if sickbeard.USE_TORRENTS and sickbeard.TORRENT_METHOD != 'blackhole' \ + and (sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'https' + or not sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'http:'): + return True else: - sickbeard.searchQueueScheduler.action.unpause_backlog() # @UndefinedVariable + return False - redirect("/manage/manageSearches/") + @staticmethod + def _getEpisode(show, season=None, episode=None, absolute=None): + if show is None: + return 'Invalid show parameters' + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return 'Invalid show paramaters' + + if absolute: + epObj = showObj.getEpisode(absolute_number=int(absolute)) + elif season and episode: + epObj = showObj.getEpisode(int(season), int(episode)) + else: + return 'Invalid paramaters' + + if epObj is None: + return "Episode couldn't be retrieved" + + return epObj + + def index(self, *args, **kwargs): + + t = PageTemplate(headers=self.request.headers, file='home.tmpl') + if sickbeard.ANIME_SPLIT_HOME: + shows = [] + anime = [] + for show in sickbeard.showList: + if show.is_anime: + anime.append(show) + else: + shows.append(show) + t.showlists = [['Shows', shows], + ['Anime', anime]] + else: + t.showlists = [['Shows', sickbeard.showList]] + + t.submenu = self.HomeMenu() + + return t.respond() + + def testSABnzbd(self, host=None, username=None, password=None, apikey=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_url(host) + + connection, accesMsg = sab.getSabAccesMethod(host, username, password, apikey) + if connection: + authed, authMsg = sab.testAuthentication(host, username, password, apikey) # @UnusedVariable + if authed: + return 'Success. Connected and authenticated' + else: + return "Authentication failed. SABnzbd expects '" + accesMsg + "' as authentication method" + else: + return 'Unable to connect to host' + + def testTorrent(self, torrent_method=None, host=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_url(host) + + client = clients.getClientIstance(torrent_method) + + connection, accesMsg = client(host, username, password).testAuthentication() + + return accesMsg + + def testGrowl(self, host=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host, default_port=23053) + + result = notifiers.growl_notifier.test_notify(host, password) + if password is None or password == '': + pw_append = '' + else: + pw_append = ' with password: ' + password + + if result: + return 'Registered and Tested growl successfully ' + urllib.unquote_plus(host) + pw_append + else: + return 'Registration and Testing of growl failed ' + urllib.unquote_plus(host) + pw_append + + def testProwl(self, prowl_api=None, prowl_priority=0): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) + if result: + return 'Test prowl notice sent successfully' + else: + return 'Test prowl notice failed' + + def testBoxcar2(self, accesstoken=None, sound=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.boxcar2_notifier.test_notify(accesstoken, sound) + if result: + return 'Boxcar2 notification succeeded. Check your Boxcar2 clients to make sure it worked' + else: + return 'Error sending Boxcar2 notification' + + def testPushover(self, userKey=None, apiKey=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.pushover_notifier.test_notify(userKey, apiKey) + if result: + return 'Pushover notification succeeded. Check your Pushover clients to make sure it worked' + else: + return 'Error sending Pushover notification' + + def twitterStep1(self, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + return notifiers.twitter_notifier._get_authorization() + + def twitterStep2(self, key): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.twitter_notifier._get_credentials(key) + logger.log(u'result: ' + str(result)) + if result: + return 'Key verification successful' + else: + return 'Unable to verify key' + + def testTwitter(self, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.twitter_notifier.test_notify() + if result: + return 'Tweet successful, check your twitter to make sure it worked' + else: + return 'Error sending tweet' + + def testXBMC(self, host=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_hosts(host) + finalResult = '' + for curHost in [x.strip() for x in host.split(',')]: + curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) + if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: + finalResult += 'Test XBMC notice sent successfully to ' + urllib.unquote_plus(curHost) + else: + finalResult += 'Test XBMC notice failed to ' + urllib.unquote_plus(curHost) + finalResult += "
\n" + + return finalResult + + def testPMC(self, host=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + finalResult = '' + for curHost in [x.strip() for x in host.split(',')]: + curResult = notifiers.plex_notifier.test_notify_pmc(urllib.unquote_plus(curHost), username, password) + if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: + finalResult += 'Successful test notice sent to Plex client ... ' + urllib.unquote_plus(curHost) + else: + finalResult += 'Test failed for Plex client ... ' + urllib.unquote_plus(curHost) + finalResult += '
' + '\n' + + ui.notifications.message('Tested Plex client(s): ', urllib.unquote_plus(host.replace(',', ', '))) + + return finalResult + + def testPMS(self, host=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + finalResult = '' + + curResult = notifiers.plex_notifier.test_notify_pms(urllib.unquote_plus(host), username, password) + if None is curResult: + finalResult += 'Successful test of Plex server(s) ... ' + urllib.unquote_plus(host.replace(',', ', ')) + else: + finalResult += 'Test failed for Plex server(s) ... ' + urllib.unquote_plus(curResult.replace(',', ', ')) + finalResult += '
' + '\n' + + ui.notifications.message('Tested Plex Media Server host(s): ', urllib.unquote_plus(host.replace(',', ', '))) + + return finalResult + + def testLibnotify(self, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + if notifiers.libnotify_notifier.test_notify(): + return 'Tried sending desktop notification via libnotify' + else: + return notifiers.libnotify.diagnose() + + def testNMJ(self, host=None, database=None, mount=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) + if result: + return 'Successfully started the scan update' + else: + return 'Test failed to start the scan update' + + def settingsNMJ(self, host=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) + if result: + return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % { + "host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} + else: + return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' + + def testNMJv2(self, host=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + result = notifiers.nmjv2_notifier.test_notify(urllib.unquote_plus(host)) + if result: + return 'Test notice sent successfully to ' + urllib.unquote_plus(host) + else: + return 'Test notice failed to ' + urllib.unquote_plus(host) + + def settingsNMJv2(self, host=None, dbloc=None, instance=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + result = notifiers.nmjv2_notifier.notify_settings(urllib.unquote_plus(host), dbloc, instance) + if result: + return '{"message": "NMJ Database found at: %(host)s", "database": "%(database)s"}' % {"host": host, + "database": sickbeard.NMJv2_DATABASE} + else: + return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { + "dbloc": dbloc} + + def testTrakt(self, api=None, username=None, password=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.trakt_notifier.test_notify(api, username, password) + if result: + return 'Test notice sent successfully to Trakt' + else: + return 'Test notice failed to Trakt' + + def loadShowNotifyLists(self, *args, **kwargs): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + myDB = db.DBConnection() + rows = myDB.select('SELECT show_id, show_name, notify_list FROM tv_shows ORDER BY show_name ASC') + + data = {} + size = 0 + for r in rows: + data[r['show_id']] = {'id': r['show_id'], 'name': r['show_name'], 'list': r['notify_list']} + size += 1 + data['_size'] = size + return json.dumps(data) + + def testEmail(self, host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + host = config.clean_host(host) + if notifiers.email_notifier.test_notify(host, port, smtp_from, use_tls, user, pwd, to): + return 'Test email sent successfully! Check inbox.' + else: + return 'ERROR: %s' % notifiers.email_notifier.last_err + + def testNMA(self, nma_api=None, nma_priority=0): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) + if result: + return 'Test NMA notice sent successfully' + else: + return 'Test NMA notice failed' + + def testPushalot(self, authorizationToken=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.pushalot_notifier.test_notify(authorizationToken) + if result: + return 'Pushalot notification succeeded. Check your Pushalot clients to make sure it worked' + else: + return 'Error sending Pushalot notification' + + def testPushbullet(self, api=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.pushbullet_notifier.test_notify(api) + if result: + return 'Pushbullet notification succeeded. Check your device to make sure it worked' + else: + return 'Error sending Pushbullet notification' + + def getPushbulletDevices(self, api=None): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + + result = notifiers.pushbullet_notifier.get_devices(api) + if result: + return result + else: + return 'Error sending Pushbullet notification' + + def shutdown(self, pid=None): + + if str(pid) != str(sickbeard.PID): + self.redirect('/home/') + + sickbeard.events.put(sickbeard.events.SystemEvent.SHUTDOWN) + + title = 'Shutting down' + message = 'SickGear is shutting down...' + + return self._genericMessage(title, message) + + def restart(self, pid=None): + + if str(pid) != str(sickbeard.PID): + self.redirect('/home/') + + t = PageTemplate(headers=self.request.headers, file='restart.tmpl') + t.submenu = self.HomeMenu() + + # restart + sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) + + return t.respond() + + def update(self, pid=None): + + if str(pid) != str(sickbeard.PID): + self.redirect('/home/') + + updated = sickbeard.versionCheckScheduler.action.update() # @UndefinedVariable + if updated: + # do a hard restart + sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) + + t = PageTemplate(headers=self.request.headers, file='restart_bare.tmpl') + return t.respond() + else: + return self._genericMessage('Update Failed', + "Update wasn't successful, not restarting. Check your log for more information.") + + def branchCheckout(self, branch): + sickbeard.BRANCH = branch + ui.notifications.message('Checking out branch: ', branch) + return self.update(sickbeard.PID) + + def pullRequestCheckout(self, branch): + pull_request = branch + branch = branch.split(':')[1] + fetched = sickbeard.versionCheckScheduler.action.fetch(pull_request) + if fetched: + sickbeard.BRANCH = branch + ui.notifications.message('Checking out branch: ', branch) + return self.update(sickbeard.PID) + else: + self.redirect('/home/') + + def displayShow(self, show=None): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + else: + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return self._genericMessage('Error', 'Show not in show list') + + myDB = db.DBConnection() + seasonResults = myDB.select( + 'SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season desc', + [showObj.indexerid] + ) + + sqlResults = myDB.select( + 'SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC', + [showObj.indexerid] + ) + + t = PageTemplate(headers=self.request.headers, file='displayShow.tmpl') + t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}] + + try: + t.showLoc = (showObj.location, True) + except sickbeard.exceptions.ShowDirNotFoundException: + t.showLoc = (showObj._location, False) + + show_message = '' + + if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): # @UndefinedVariable + show_message = 'This show is in the process of being downloaded - the info below is incomplete.' + + elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable + show_message = 'The information on this page is in the process of being updated.' + + elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): # @UndefinedVariable + show_message = 'The episodes below are currently being refreshed from disk' + + elif sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj): # @UndefinedVariable + show_message = 'Currently downloading subtitles for this show' + + elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): # @UndefinedVariable + show_message = 'This show is queued to be refreshed.' + + elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): # @UndefinedVariable + show_message = 'This show is queued and awaiting an update.' + + elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(showObj): # @UndefinedVariable + show_message = 'This show is queued and awaiting subtitles download.' + + if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): # @UndefinedVariable + if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable + t.submenu.append( + {'title': 'Remove', 'path': 'home/deleteShow?show=%d' % showObj.indexerid, 'confirm': True}) + t.submenu.append({'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d' % showObj.indexerid}) + t.submenu.append( + {'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1' % showObj.indexerid}) + t.submenu.append({'title': 'Update show in XBMC', + 'path': 'home/updateXBMC?showName=%s' % urllib.quote_plus( + showObj.name.encode('utf-8')), 'requires': self.haveXBMC}) + t.submenu.append({'title': 'Preview Rename', 'path': 'home/testRename?show=%d' % showObj.indexerid}) + if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( + showObj) and showObj.subtitles: + t.submenu.append( + {'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d' % showObj.indexerid}) + + t.show = showObj + t.sqlResults = sqlResults + t.seasonResults = seasonResults + t.show_message = show_message + + epCounts = {} + epCats = {} + epCounts[Overview.SKIPPED] = 0 + epCounts[Overview.WANTED] = 0 + epCounts[Overview.QUAL] = 0 + epCounts[Overview.GOOD] = 0 + epCounts[Overview.UNAIRED] = 0 + epCounts[Overview.SNATCHED] = 0 + + for curResult in sqlResults: + curEpCat = showObj.getOverview(int(curResult['status'])) + if curEpCat: + epCats[str(curResult['season']) + 'x' + str(curResult['episode'])] = curEpCat + epCounts[curEpCat] += 1 + + def titler(x): + return (remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] + + if sickbeard.ANIME_SPLIT_HOME: + shows = [] + anime = [] + for show in sickbeard.showList: + if show.is_anime: + anime.append(show) + else: + shows.append(show) + t.sortedShowLists = [['Shows', sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))], + ['Anime', sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + + else: + t.sortedShowLists = [ + ['Shows', sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + + tvshows = [] + for tvshow_types in t.sortedShowLists: + for tvshow in tvshow_types[1]: + tvshows.append(tvshow.indexerid) + t.tvshow_id_csv = ','.join(str(x) for x in tvshows) + + t.bwl = None + if showObj.is_anime: + t.bwl = showObj.release_groups + + t.epCounts = epCounts + t.epCats = epCats + + showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.indexerid) + + indexerid = int(showObj.indexerid) + indexer = int(showObj.indexer) + t.all_scene_exceptions = showObj.exceptions + t.scene_numbering = get_scene_numbering_for_show(indexerid, indexer) + t.xem_numbering = get_xem_numbering_for_show(indexerid, indexer) + t.scene_absolute_numbering = get_scene_absolute_numbering_for_show(indexerid, indexer) + t.xem_absolute_numbering = get_xem_absolute_numbering_for_show(indexerid, indexer) + + return t.respond() + + def plotDetails(self, show, season, episode): + myDB = db.DBConnection() + result = myDB.select( + 'SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?', + (int(show), int(season), int(episode))) + return result[0]['description'] if result else 'Episode not found.' + + def sceneExceptions(self, show): + exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(show) + if not exceptionsList: + return 'No scene exceptions' + + out = [] + for season, names in iter(sorted(exceptionsList.iteritems())): + if season == -1: + season = '*' + out.append('S' + str(season) + ': ' + ', '.join(names)) + return '
'.join(out) + + def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], exceptions_list=[], + flatten_folders=None, paused=None, directCall=False, air_by_date=None, sports=None, dvdorder=None, + indexerLang=None, subtitles=None, archive_firstmatch=None, rls_ignore_words=None, + rls_require_words=None, anime=None, blacklist=None, whitelist=None, + scene=None): + + if show is None: + errString = 'Invalid show ID: ' + str(show) + if directCall: + return [errString] + else: + return self._genericMessage('Error', errString) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if not showObj: + errString = 'Unable to find the specified show: ' + str(show) + if directCall: + return [errString] + else: + return self._genericMessage('Error', errString) + + showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.indexerid) + + if not location and not anyQualities and not bestQualities and not flatten_folders: + t = PageTemplate(headers=self.request.headers, file='editShow.tmpl') + t.submenu = self.HomeMenu() + + if showObj.is_anime: + t.whitelist = showObj.release_groups.whitelist + t.blacklist = showObj.release_groups.blacklist + + t.groups = [] + if helpers.set_up_anidb_connection(): + try: + anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=showObj.name) + t.groups = anime.get_groups() + except Exception, e: + t.groups.append(dict([('name', 'Fail:AniDB connect. Restart sg else check debug log'), ('rating', ''), ('range', '')])) + else: + t.groups.append(dict([('name', 'Did not initialise AniDB. Check debug log if reqd.'), ('rating', ''), ('range', '')])) + + with showObj.lock: + t.show = showObj + t.scene_exceptions = get_scene_exceptions(showObj.indexerid) + + return t.respond() + + flatten_folders = config.checkbox_to_value(flatten_folders) + dvdorder = config.checkbox_to_value(dvdorder) + archive_firstmatch = config.checkbox_to_value(archive_firstmatch) + paused = config.checkbox_to_value(paused) + air_by_date = config.checkbox_to_value(air_by_date) + scene = config.checkbox_to_value(scene) + sports = config.checkbox_to_value(sports) + anime = config.checkbox_to_value(anime) + subtitles = config.checkbox_to_value(subtitles) + + if indexerLang and indexerLang in sickbeard.indexerApi(showObj.indexer).indexer().config['valid_languages']: + indexer_lang = indexerLang + else: + indexer_lang = showObj.lang + + # if we changed the language then kick off an update + if indexer_lang == showObj.lang: + do_update = False + else: + do_update = True + + if scene == showObj.scene and anime == showObj.anime: + do_update_scene_numbering = False + else: + do_update_scene_numbering = True + + if type(anyQualities) != list: + anyQualities = [anyQualities] + + if type(bestQualities) != list: + bestQualities = [bestQualities] + + if type(exceptions_list) != list: + exceptions_list = [exceptions_list] + + # If directCall from mass_edit_update no scene exceptions handling or blackandwhite list handling + if directCall: + do_update_exceptions = False + else: + if set(exceptions_list) == set(showObj.exceptions): + do_update_exceptions = False + else: + do_update_exceptions = True + + with showObj.lock: + if anime: + if not showObj.release_groups: + showObj.release_groups = BlackAndWhiteList(showObj.indexerid) + if whitelist: + shortwhitelist = short_group_names(whitelist) + showObj.release_groups.set_white_keywords(shortwhitelist) + else: + showObj.release_groups.set_white_keywords([]) + + if blacklist: + shortblacklist = short_group_names(blacklist) + showObj.release_groups.set_black_keywords(shortblacklist) + else: + showObj.release_groups.set_black_keywords([]) + + errors = [] + with showObj.lock: + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + showObj.quality = newQuality + showObj.archive_firstmatch = archive_firstmatch + + # reversed for now + if bool(showObj.flatten_folders) != bool(flatten_folders): + showObj.flatten_folders = flatten_folders + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable + except exceptions.CantRefreshException, e: + errors.append('Unable to refresh this show: ' + ex(e)) + + showObj.paused = paused + showObj.scene = scene + showObj.anime = anime + showObj.sports = sports + showObj.subtitles = subtitles + showObj.air_by_date = air_by_date + + if not directCall: + showObj.lang = indexer_lang + showObj.dvdorder = dvdorder + showObj.rls_ignore_words = rls_ignore_words.strip() + showObj.rls_require_words = rls_require_words.strip() + + # if we change location clear the db of episodes, change it, write to db, and rescan + if os.path.normpath(showObj._location) != os.path.normpath(location): + logger.log(os.path.normpath(showObj._location) + ' != ' + os.path.normpath(location), logger.DEBUG) + if not ek.ek(os.path.isdir, location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: + errors.append('New location %s does not exist' % location) + + # don't bother if we're going to update anyway + elif not do_update: + # change it + try: + showObj.location = location + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable + except exceptions.CantRefreshException, e: + errors.append('Unable to refresh this show:' + ex(e)) + # grab updated info from TVDB + # showObj.loadEpisodesFromIndexer() + # rescan the episodes in the new folder + except exceptions.NoNFOException: + errors.append( + "The folder at %s doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in SickGear." % location) + + # save it to the DB + showObj.saveToDB() + + # force the update + if do_update: + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, True) # @UndefinedVariable + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + except exceptions.CantUpdateException, e: + errors.append('Unable to force an update on the show.') + + if do_update_exceptions: + try: + scene_exceptions.update_scene_exceptions(showObj.indexerid, exceptions_list) # @UndefinedVdexerid) + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + except exceptions.CantUpdateException, e: + errors.append('Unable to force an update on scene exceptions of the show.') + + if do_update_scene_numbering: + try: + sickbeard.scene_numbering.xem_refresh(showObj.indexerid, showObj.indexer) # @UndefinedVariable + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + except exceptions.CantUpdateException, e: + errors.append('Unable to force an update on scene numbering of the show.') + + if directCall: + return errors + + if len(errors) > 0: + ui.notifications.error('%d error%s while saving changes:' % (len(errors), '' if len(errors) == 1 else 's'), + '
    ' + '\n'.join(['
  • %s
  • ' % error for error in errors]) + '
') + + self.redirect('/home/displayShow?show=' + show) + + def deleteShow(self, show=None, full=0): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return self._genericMessage('Error', 'Unable to find the specified show') + + if sickbeard.showQueueScheduler.action.isBeingAdded( + showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable + return self._genericMessage("Error", "Shows can't be deleted while they're being added or updated.") + + if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC: + # remove show from trakt.tv library + sickbeard.traktCheckerScheduler.action.removeShowFromTraktLibrary(showObj) + + showObj.deleteShow(bool(full)) + + ui.notifications.message('%s with %s' % (('Deleting', 'Trashing')[sickbeard.TRASH_REMOVE_SHOW], + ('media left untouched', 'all related media')[bool(full)]), + '%s' % showObj.name) + self.redirect('/home/') + + def refreshShow(self, show=None): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return self._genericMessage('Error', 'Unable to find the specified show') + + # force the update from the DB + try: + sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable + except exceptions.CantRefreshException, e: + ui.notifications.error('Unable to refresh this show.', + ex(e)) + + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + self.redirect('/home/displayShow?show=' + str(showObj.indexerid)) + + def updateShow(self, show=None, force=0): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return self._genericMessage('Error', 'Unable to find the specified show') + + # force the update + try: + sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) # @UndefinedVariable + except exceptions.CantUpdateException, e: + ui.notifications.error('Unable to update this show.', + ex(e)) + + # just give it some time + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + self.redirect('/home/displayShow?show=' + str(showObj.indexerid)) + + def subtitleShow(self, show=None, force=0): + + if show is None: + return self._genericMessage('Error', 'Invalid show ID') + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return self._genericMessage('Error', 'Unable to find the specified show') + + # search and download subtitles + sickbeard.showQueueScheduler.action.downloadSubtitles(showObj, bool(force)) # @UndefinedVariable + + time.sleep(cpu_presets[sickbeard.CPU_PRESET]) + + self.redirect('/home/displayShow?show=' + str(showObj.indexerid)) + + def updateXBMC(self, showName=None): + + # only send update to first host in the list -- workaround for xbmc sql backend users + if sickbeard.XBMC_UPDATE_ONLYFIRST: + # only send update to first host in the list -- workaround for xbmc sql backend users + host = sickbeard.XBMC_HOST.split(',')[0].strip() + else: + host = sickbeard.XBMC_HOST + + if notifiers.xbmc_notifier.update_library(showName=showName): + ui.notifications.message('Library update command sent to XBMC host(s): ' + host) + else: + ui.notifications.error('Unable to contact one or more XBMC host(s): ' + host) + self.redirect('/home/') + + def updatePLEX(self, *args, **kwargs): + result = notifiers.plex_notifier.update_library() + if None is result: + ui.notifications.message( + 'Library update command sent to', 'Plex Media Server host(s): ' + sickbeard.PLEX_SERVER_HOST.replace(',', ', ')) + else: + ui.notifications.error('Unable to contact', 'Plex Media Server host(s): ' + result.replace(',', ', ')) + self.redirect('/home/') + + def setStatus(self, show=None, eps=None, status=None, direct=False): + + if show is None or eps is None or status is None: + errMsg = 'You must specify a show and at least one episode' + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return self._genericMessage('Error', errMsg) + + if not statusStrings.has_key(int(status)): + errMsg = 'Invalid status' + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return self._genericMessage('Error', errMsg) + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + errMsg = 'Error', 'Show not in show list' + if direct: + ui.notifications.error('Error', errMsg) + return json.dumps({'result': 'error'}) + else: + return self._genericMessage('Error', errMsg) + + segments = {} + if eps is not None: + + sql_l = [] + for curEp in eps.split('|'): + + logger.log(u'Attempting to set status on episode ' + curEp + ' to ' + status, logger.DEBUG) + + epInfo = curEp.split('x') + + epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) + + if epObj is None: + return self._genericMessage("Error", "Episode couldn't be retrieved") + + if int(status) in [WANTED, FAILED]: + # figure out what episodes are wanted so we can backlog them + if epObj.season in segments: + segments[epObj.season].append(epObj) + else: + segments[epObj.season] = [epObj] + + with epObj.lock: + # don't let them mess up UNAIRED episodes + if epObj.status == UNAIRED: + logger.log(u'Refusing to change status of ' + curEp + ' because it is UNAIRED', logger.ERROR) + continue + + if int( + status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED + [ + IGNORED] and not ek.ek(os.path.isfile, epObj.location): + logger.log( + u'Refusing to change status of ' + curEp + " to DOWNLOADED because it's not SNATCHED/DOWNLOADED", + logger.ERROR) + continue + + if int( + status) == FAILED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED: + logger.log( + u'Refusing to change status of ' + curEp + " to FAILED because it's not SNATCHED/DOWNLOADED", + logger.ERROR) + continue + + epObj.status = int(status) + + # mass add to database + result = epObj.get_sql() + if None is not result: + sql_l.append(result) + + if len(sql_l) > 0: + myDB = db.DBConnection() + myDB.mass_action(sql_l) + + if int(status) == WANTED: + msg = 'Backlog was automatically started for the following seasons of ' + showObj.name + ':
' + msg += '
    ' + + for season, segment in segments.items(): + cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment) + sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) # @UndefinedVariable + + msg += '
  • Season ' + str(season) + '
  • ' + logger.log(u'Sending backlog for ' + showObj.name + ' season ' + str( + season) + ' because some eps were set to wanted') + + msg += '
' + + if segments: + ui.notifications.message('Backlog started', msg) + + if int(status) == FAILED: + msg = 'Retrying Search was automatically started for the following season of ' + showObj.name + ':
' + msg += '
    ' + + for season, segment in segments.items(): + cur_failed_queue_item = search_queue.FailedQueueItem(showObj, [segment]) + sickbeard.searchQueueScheduler.action.add_item(cur_failed_queue_item) # @UndefinedVariable + + msg += '
  • Season ' + str(season) + '
  • ' + logger.log(u'Retrying Search for ' + showObj.name + ' season ' + str( + season) + ' because some eps were set to failed') + + msg += '
' + + if segments: + ui.notifications.message('Retry Search started', msg) + + if direct: + return json.dumps({'result': 'success'}) + else: + self.redirect('/home/displayShow?show=' + show) + + def testRename(self, show=None): + + if show is None: + return self._genericMessage('Error', 'You must specify a show') + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj is None: + return self._genericMessage('Error', 'Show not in show list') + + try: + show_loc = showObj.location # @UnusedVariable + except exceptions.ShowDirNotFoundException: + return self._genericMessage('Error', "Can't rename episodes when the show dir is missing.") + + ep_obj_rename_list = [] + + ep_obj_list = showObj.getAllEpisodes(has_location=True) + + for cur_ep_obj in ep_obj_list: + # Only want to rename if we have a location + if cur_ep_obj.location: + if cur_ep_obj.relatedEps: + # do we have one of multi-episodes in the rename list already + have_already = False + for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: + if cur_related_ep in ep_obj_rename_list: + have_already = True + break + if not have_already: + ep_obj_rename_list.append(cur_ep_obj) + else: + ep_obj_rename_list.append(cur_ep_obj) + + if ep_obj_rename_list: + # present season DESC episode DESC on screen + ep_obj_rename_list.reverse() + + t = PageTemplate(headers=self.request.headers, file='testRename.tmpl') + t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}] + t.ep_obj_list = ep_obj_rename_list + t.show = showObj + + return t.respond() + + def doRename(self, show=None, eps=None): + + if show is None or eps is None: + errMsg = 'You must specify a show and at least one episode' + return self._genericMessage('Error', errMsg) + + show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if show_obj is None: + errMsg = 'Error', 'Show not in show list' + return self._genericMessage('Error', errMsg) + + try: + show_loc = show_obj.location # @UnusedVariable + except exceptions.ShowDirNotFoundException: + return self._genericMessage('Error', "Can't rename episodes when the show dir is missing.") + + if eps is None: + self.redirect('/home/displayShow?show=' + show) + + myDB = db.DBConnection() + for curEp in eps.split('|'): + + epInfo = curEp.split('x') + + # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database + ep_result = myDB.select( + 'SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5', + [show, epInfo[0], epInfo[1]]) + if not ep_result: + logger.log(u'Unable to find an episode for ' + curEp + ', skipping', logger.WARNING) + continue + related_eps_result = myDB.select('SELECT * FROM tv_episodes WHERE location = ? AND episode != ?', + [ep_result[0]['location'], epInfo[1]]) + + root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) + root_ep_obj.relatedEps = [] + + for cur_related_ep in related_eps_result: + related_ep_obj = show_obj.getEpisode(int(cur_related_ep['season']), int(cur_related_ep['episode'])) + if related_ep_obj not in root_ep_obj.relatedEps: + root_ep_obj.relatedEps.append(related_ep_obj) + + root_ep_obj.rename() + + self.redirect('/home/displayShow?show=' + show) + + def searchEpisode(self, show=None, season=None, episode=None): + + # retrieve the episode object and fail if we can't get one + ep_obj = self._getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj.show, ep_obj) + + sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable + + if ep_queue_item.success: + return returnManualSearchResult(ep_queue_item) + if not ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({'result': 'success'}) #I Actually want to call it queued, because the search hasnt been started yet! + if ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({'result': 'success'}) + else: + return json.dumps({'result': 'failure'}) + + ### Returns the current ep_queue_item status for the current viewed show. + # Possible status: Downloaded, Snatched, etc... + # Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013'] + def getManualSearchStatus(self, show=None, season=None): + + episodes = [] + currentManualSearchThreadsQueued = [] + currentManualSearchThreadActive = [] + finishedManualSearchThreadItems= [] + + # Queued Searches + currentManualSearchThreadsQueued = sickbeard.searchQueueScheduler.action.get_all_ep_from_queue(show) + # Running Searches + if (sickbeard.searchQueueScheduler.action.is_manualsearch_in_progress()): + currentManualSearchThreadActive = sickbeard.searchQueueScheduler.action.currentItem + + # Finished Searches + finishedManualSearchThreadItems = sickbeard.search_queue.MANUAL_SEARCH_HISTORY + + if currentManualSearchThreadsQueued: + for searchThread in currentManualSearchThreadsQueued: + searchstatus = 'queued' + if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): + episodes.append({'episode': searchThread.segment.episode, + 'episodeindexid': searchThread.segment.indexerid, + 'season' : searchThread.segment.season, + 'searchstatus' : searchstatus, + 'status' : statusStrings[searchThread.segment.status], + 'quality': self.getQualityClass(searchThread.segment)}) + else: + for epObj in searchThread.segment: + episodes.append({'episode': epObj.episode, + 'episodeindexid': epObj.indexerid, + 'season' : epObj.season, + 'searchstatus' : searchstatus, + 'status' : statusStrings[epObj.status], + 'quality': self.getQualityClass(epObj)}) + + if currentManualSearchThreadActive: + searchThread = currentManualSearchThreadActive + searchstatus = 'searching' + if searchThread.success: + searchstatus = 'finished' + else: + searchstatus = 'searching' + episodes.append({'episode': searchThread.segment.episode, + 'episodeindexid': searchThread.segment.indexerid, + 'season' : searchThread.segment.season, + 'searchstatus' : searchstatus, + 'status' : statusStrings[searchThread.segment.status], + 'quality': self.getQualityClass(searchThread.segment)}) + + if finishedManualSearchThreadItems: + for searchThread in finishedManualSearchThreadItems: + if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): + if str(searchThread.show.indexerid) == show and not [x for x in episodes if x['episodeindexid'] == searchThread.segment.indexerid]: + searchstatus = 'finished' + episodes.append({'episode': searchThread.segment.episode, + 'episodeindexid': searchThread.segment.indexerid, + 'season' : searchThread.segment.season, + 'searchstatus' : searchstatus, + 'status' : statusStrings[searchThread.segment.status], + 'quality': self.getQualityClass(searchThread.segment)}) + else: + ### These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segement/episodes + if str(searchThread.show.indexerid) == show: + for epObj in searchThread.segment: + if not [x for x in episodes if x['episodeindexid'] == epObj.indexerid]: + searchstatus = 'finished' + episodes.append({'episode': epObj.episode, + 'episodeindexid': epObj.indexerid, + 'season' : epObj.season, + 'searchstatus' : searchstatus, + 'status' : statusStrings[epObj.status], + 'quality': self.getQualityClass(epObj)}) + + return json.dumps({'show': show, 'episodes' : episodes}) + + #return json.dumps() + + def getQualityClass(self, ep_obj): + # return the correct json value + + # Find the quality class for the episode + quality_class = Quality.qualityStrings[Quality.UNKNOWN] + ep_status, ep_quality = Quality.splitCompositeStatus(ep_obj.status) + for x in (SD, HD720p, HD1080p): + if ep_quality in Quality.splitQuality(x)[0]: + quality_class = qualityPresetStrings[x] + break + + return quality_class + + def searchEpisodeSubtitles(self, show=None, season=None, episode=None): + # retrieve the episode object and fail if we can't get one + ep_obj = self._getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # try do download subtitles for that episode + previous_subtitles = set(subliminal.language.Language(x) for x in ep_obj.subtitles) + try: + ep_obj.subtitles = set(x.language for x in ep_obj.downloadSubtitles().values()[0]) + except: + return json.dumps({'result': 'failure'}) + + # return the correct json value + if previous_subtitles != ep_obj.subtitles: + status = 'New subtitles downloaded: %s' % ' '.join([ + "" + x.name + "" for x in + sorted(list(ep_obj.subtitles.difference(previous_subtitles)))]) + else: + status = 'No subtitles downloaded' + ui.notifications.message('Subtitles Search', status) + return json.dumps({'result': status, 'subtitles': ','.join(sorted([x.alpha2 for x in + ep_obj.subtitles.union(previous_subtitles)]))}) + + def setSceneNumbering(self, show, indexer, forSeason=None, forEpisode=None, forAbsolute=None, sceneSeason=None, + sceneEpisode=None, sceneAbsolute=None): + + # sanitize: + if forSeason in ['null', '']: forSeason = None + if forEpisode in ['null', '']: forEpisode = None + if forAbsolute in ['null', '']: forAbsolute = None + if sceneSeason in ['null', '']: sceneSeason = None + if sceneEpisode in ['null', '']: sceneEpisode = None + if sceneAbsolute in ['null', '']: sceneAbsolute = None + + showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) + + if showObj.is_anime: + result = { + 'success': True, + 'forAbsolute': forAbsolute, + } + else: + result = { + 'success': True, + 'forSeason': forSeason, + 'forEpisode': forEpisode, + } + + # retrieve the episode object and fail if we can't get one + if showObj.is_anime: + ep_obj = self._getEpisode(show, absolute=forAbsolute) + else: + ep_obj = self._getEpisode(show, forSeason, forEpisode) + + if isinstance(ep_obj, str): + result['success'] = False + result['errorMessage'] = ep_obj + elif showObj.is_anime: + logger.log(u'setAbsoluteSceneNumbering for %s from %s to %s' % + (show, forAbsolute, sceneAbsolute), logger.DEBUG) + + show = int(show) + indexer = int(indexer) + forAbsolute = int(forAbsolute) + if sceneAbsolute is not None: sceneAbsolute = int(sceneAbsolute) + + set_scene_numbering(show, indexer, absolute_number=forAbsolute, sceneAbsolute=sceneAbsolute) + else: + logger.log(u'setEpisodeSceneNumbering for %s from %sx%s to %sx%s' % + (show, forSeason, forEpisode, sceneSeason, sceneEpisode), logger.DEBUG) + + show = int(show) + indexer = int(indexer) + forSeason = int(forSeason) + forEpisode = int(forEpisode) + if sceneSeason is not None: sceneSeason = int(sceneSeason) + if sceneEpisode is not None: sceneEpisode = int(sceneEpisode) + + set_scene_numbering(show, indexer, season=forSeason, episode=forEpisode, sceneSeason=sceneSeason, + sceneEpisode=sceneEpisode) + + if showObj.is_anime: + sn = get_scene_absolute_numbering(show, indexer, forAbsolute) + if sn: + result['sceneAbsolute'] = sn + else: + result['sceneAbsolute'] = None + else: + sn = get_scene_numbering(show, indexer, forSeason, forEpisode) + if sn: + (result['sceneSeason'], result['sceneEpisode']) = sn + else: + (result['sceneSeason'], result['sceneEpisode']) = (None, None) + + return json.dumps(result) + + def retryEpisode(self, show, season, episode): + + # retrieve the episode object and fail if we can't get one + ep_obj = self._getEpisode(show, season, episode) + if isinstance(ep_obj, str): + return json.dumps({'result': 'failure'}) + + # make a queue item for it and put it on the queue + ep_queue_item = search_queue.FailedQueueItem(ep_obj.show, [ep_obj]) + sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable + + if ep_queue_item.success: + return returnManualSearchResult(ep_queue_item) + if not ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({'result': 'success'}) #I Actually want to call it queued, because the search hasnt been started yet! + if ep_queue_item.started and ep_queue_item.success is None: + return json.dumps({'result': 'success'}) + else: + return json.dumps({'result': 'failure'}) + + @staticmethod + def fetch_releasegroups(show_name): + + if helpers.set_up_anidb_connection(): + try: + anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=show_name) + groups = anime.get_groups() + except Exception, e: + logger.log(u'exception msg: ' + str(e), logger.DEBUG) + return json.dumps({'result': 'fail', 'resp': 'connect'}) + + return json.dumps({'result': 'success', 'groups': groups}) + + return json.dumps({'result': 'fail', 'resp': 'init'}) + + +class HomePostProcess(Home): + def index(self, *args, **kwargs): + + t = PageTemplate(headers=self.request.headers, file='home_postprocess.tmpl') + t.submenu = self.HomeMenu() + return t.respond() + + def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None, process_method=None, force=None, + is_priority=None, failed='0', type='auto', *args, **kwargs): + + if failed == '0': + failed = False + else: + failed = True + + if force in ['on', '1']: + force = True + else: + force = False + + if is_priority in ['on', '1']: + is_priority = True + else: + is_priority = False + + if not dir: + self.redirect('/home/postprocess/') + else: + result = processTV.processDir(dir, nzbName, process_method=process_method, force=force, + is_priority=is_priority, failed=failed, type=type) + if quiet is not None and int(quiet) == 1: + return result + + result = result.replace('\n', '
\n') + return self._genericMessage('Postprocessing results', result) + + +class NewHomeAddShows(Home): + def index(self, *args, **kwargs): + + t = PageTemplate(headers=self.request.headers, file='home_addShows.tmpl') + t.submenu = self.HomeMenu() + return t.respond() + + def getIndexerLanguages(self, *args, **kwargs): + result = sickbeard.indexerApi().config['valid_languages'] + + # Make sure list is sorted alphabetically but 'en' is in front + if 'en' in result: + del result[result.index('en')] + result.sort() + result.insert(0, 'en') + + return json.dumps({'results': result}) + + def sanitizeFileName(self, name): + return helpers.sanitizeFileName(name) + + def searchIndexersForShowName(self, search_term, lang='en', indexer=None): + if not lang or lang == 'null': + lang = 'en' + + search_term = search_term.encode('utf-8') + + results = {} + final_results = [] + + # Query Indexers for each search term and build the list of results + for indexer in sickbeard.indexerApi().indexers if not int(indexer) else [int(indexer)]: + lINDEXER_API_PARMS = sickbeard.indexerApi(indexer).api_params.copy() + lINDEXER_API_PARMS['language'] = lang + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsListUI + t = sickbeard.indexerApi(indexer).indexer(**lINDEXER_API_PARMS) + + logger.log('Searching for Show with searchterm: %s on Indexer: %s' % ( + search_term, sickbeard.indexerApi(indexer).name), logger.DEBUG) + try: + # add search results + results.setdefault(indexer, []).extend(t[search_term]) + except Exception, e: + continue + + map(final_results.extend, + ([[sickbeard.indexerApi(id).name, id, sickbeard.indexerApi(id).config['show_url'], int(show['id']), + show['seriesname'], show['firstaired']] for show in shows] for id, shows in results.items())) + + lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] + return json.dumps({'results': final_results, 'langid': lang_id}) + + def massAddTable(self, rootDir=None): + t = PageTemplate(headers=self.request.headers, file='home_massAddTable.tmpl') + t.submenu = self.HomeMenu() + + if not rootDir: + return 'No folders selected.' + elif type(rootDir) != list: + root_dirs = [rootDir] + else: + root_dirs = rootDir + + root_dirs = [urllib.unquote_plus(x) for x in root_dirs] + + if sickbeard.ROOT_DIRS: + default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) + else: + default_index = 0 + + if len(root_dirs) > default_index: + tmp = root_dirs[default_index] + if tmp in root_dirs: + root_dirs.remove(tmp) + root_dirs = [tmp] + root_dirs + + dir_list = [] + + myDB = db.DBConnection() + for root_dir in root_dirs: + try: + file_list = ek.ek(os.listdir, root_dir) + except: + continue + + for cur_file in file_list: + + cur_path = ek.ek(os.path.normpath, ek.ek(os.path.join, root_dir, cur_file)) + if not ek.ek(os.path.isdir, cur_path): + continue + + cur_dir = { + 'dir': cur_path, + 'display_dir': '' + ek.ek(os.path.dirname, cur_path) + os.sep + '' + ek.ek( + os.path.basename, + cur_path), + } + + # see if the folder is in XBMC already + dirResults = myDB.select('SELECT * FROM tv_shows WHERE location = ?', [cur_path]) + + if dirResults: + cur_dir['added_already'] = True + else: + cur_dir['added_already'] = False + + dir_list.append(cur_dir) + + indexer_id = show_name = indexer = None + for cur_provider in sickbeard.metadata_provider_dict.values(): + if indexer_id and show_name: + continue + + (indexer_id, show_name, indexer) = cur_provider.retrieveShowMetadata(cur_path) + + # default to TVDB if indexer was not detected + if show_name and not (indexer or indexer_id): + (sn, idx, id) = helpers.searchIndexerForShowID(show_name, indexer, indexer_id) + + # set indexer and indexer_id from found info + if not indexer and idx: + indexer = idx + + if not indexer_id and id: + indexer_id = id + + cur_dir['existing_info'] = (indexer_id, show_name, indexer) + + if indexer_id and helpers.findCertainShow(sickbeard.showList, indexer_id): + cur_dir['added_already'] = True + + t.dirList = dir_list + + return t.respond() + + def newShow(self, show_to_add=None, other_shows=None, use_show_name=None): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + self.set_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.set_header('Pragma', 'no-cache') + self.set_header('Expires', '0') + + t = PageTemplate(headers=self.request.headers, file='home_newShow.tmpl') + t.submenu = self.HomeMenu() + + indexer, show_dir, indexer_id, show_name = self.split_extra_show(show_to_add) + + if indexer_id and indexer and show_name: + use_provided_info = True + else: + use_provided_info = False + + # tell the template whether we're giving it show name & Indexer ID + t.use_provided_info = use_provided_info + + # use the given show_dir for the indexer search if available + if use_show_name: + t.default_show_name = show_name + elif not show_dir: + t.default_show_name = '' + elif not show_name: + t.default_show_name = ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.', ' ') + else: + t.default_show_name = show_name + + # carry a list of other dirs if given + if not other_shows: + other_shows = [] + elif type(other_shows) != list: + other_shows = [other_shows] + + if use_provided_info: + t.provided_indexer_id = int(indexer_id or 0) + t.provided_indexer_name = show_name + + t.provided_show_dir = show_dir + t.other_shows = other_shows + t.provided_indexer = int(indexer or sickbeard.INDEXER_DEFAULT) + t.indexers = sickbeard.indexerApi().indexers + t.whitelist = [] + t.blacklist = [] + t.groups = [] + + return t.respond() + + def recommendedShows(self, *args, **kwargs): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + self.set_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.set_header('Pragma', 'no-cache') + self.set_header('Expires', '0') + + t = PageTemplate(headers=self.request.headers, file='home_recommendedShows.tmpl') + t.submenu = self.HomeMenu() + + return t.respond() + + def getRecommendedShows(self, *args, **kwargs): + final_results = [] + + logger.log(u'Getting recommended shows from Trakt.tv', logger.DEBUG) + recommendedlist = TraktCall('recommendations/shows.json/%API%', sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, + sickbeard.TRAKT_PASSWORD) + + if recommendedlist == 'NULL': + logger.log(u'No shows found in your recommendedlist, aborting recommendedlist update', logger.DEBUG) + return + + if recommendedlist is None: + logger.log(u'Could not connect to trakt service, aborting recommended list update', logger.ERROR) + return + + map(final_results.append, + ([show['url'], + show['title'], + show['overview'], + sbdatetime.sbdatetime.sbfdate(datetime.date.fromtimestamp(int(show['first_aired']))), + sickbeard.indexerApi(1).name, + sickbeard.indexerApi(1).config['icon'], + int(show['tvdb_id'] or 0), + '%s%s' % (sickbeard.indexerApi(1).config['show_url'], int(show['tvdb_id'] or 0)), + sickbeard.indexerApi(2).name, + sickbeard.indexerApi(2).config['icon'], + int(show['tvrage_id'] or 0), + '%s%s' % (sickbeard.indexerApi(2).config['show_url'], int(show['tvrage_id'] or 0)) + ] for show in recommendedlist if not helpers.findCertainShow(sickbeard.showList, indexerid=int(show['tvdb_id'])))) + + self.set_header('Content-Type', 'application/json') + return json.dumps({'results': final_results}) + + def addRecommendedShow(self, whichSeries=None, indexerLang='en', rootDir=None, defaultStatus=None, + anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, + fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, + scene=None): + + indexer = 1 + indexer_name = sickbeard.indexerApi(int(indexer)).name + show_url = whichSeries.split('|')[1] + indexer_id = whichSeries.split('|')[0] + show_name = whichSeries.split('|')[2] + + return self.addNewShow('|'.join([indexer_name, str(indexer), show_url, indexer_id, show_name, '']), + indexerLang, rootDir, + defaultStatus, + anyQualities, bestQualities, flatten_folders, subtitles, fullShowPath, other_shows, + skipShow, providedIndexer, anime, scene) + + def trendingShows(self, *args, **kwargs): + """ + Display the new show page which collects a tvdb id, folder, and extra options and + posts them to addNewShow + """ + t = PageTemplate(headers=self.request.headers, file='home_trendingShows.tmpl') + t.submenu = self.HomeMenu() + + t.trending_shows = TraktCall('shows/trending.json/%API%', sickbeard.TRAKT_API_KEY) + t.trending_inlibrary = 0 + if None is not t.trending_shows: + for item in t.trending_shows: + tvdbs = ['tvdb_id', 'tvrage_id'] + for index, tvdb in enumerate(tvdbs): + try: + item[u'show_id'] = str(item[tvdb]) + tvshow = helpers.findCertainShow(sickbeard.showList, int(item[tvdb])) + except: + continue + # check tvshow indexer is not using the same id from another indexer + if tvshow and (index + 1) == tvshow.indexer: + item[u'show_id'] = u'%s:%s' % (tvshow.indexer, item[tvdb]) + t.trending_inlibrary += 1 + break + + return t.respond() + + def existingShows(self, *args, **kwargs): + """ + Prints out the page to add existing shows from a root dir + """ + t = PageTemplate(headers=self.request.headers, file='home_addExistingShow.tmpl') + t.submenu = self.HomeMenu() + t.whitelist = [] + t.blacklist = [] + t.groups = [] + + return t.respond() + + def addTraktShow(self, indexer_id, showName): + if helpers.findCertainShow(sickbeard.showList, int(indexer_id)): + return + return self.newShow('|'.join(['', '', indexer_id, showName]), use_show_name=True) + + def addNewShow(self, whichSeries=None, indexerLang='en', rootDir=None, defaultStatus=None, + anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, + fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, + scene=None, blacklist=None, whitelist=None): + """ + Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are + provided then it forwards back to newShow, if not it goes to /home. + """ + + # grab our list of other dirs if given + if not other_shows: + other_shows = [] + elif type(other_shows) != list: + other_shows = [other_shows] + + def finishAddShow(): + # if there are no extra shows then go home + if not other_shows: + self.redirect('/home/') + + # peel off the next one + next_show_dir = other_shows[0] + rest_of_show_dirs = other_shows[1:] + + # go to add the next show + return self.newShow(next_show_dir, rest_of_show_dirs) + + # if we're skipping then behave accordingly + if skipShow: + return finishAddShow() + + # sanity check on our inputs + if (not rootDir and not fullShowPath) or not whichSeries: + return 'Missing params, no Indexer ID or folder:' + repr(whichSeries) + ' and ' + repr( + rootDir) + '/' + repr(fullShowPath) + + # figure out what show we're adding and where + series_pieces = whichSeries.split('|') + if (whichSeries and rootDir) or (whichSeries and fullShowPath and len(series_pieces) > 1): + if len(series_pieces) < 6: + logger.log('Unable to add show due to show selection. Not enough arguments: %s' % (repr(series_pieces)), + logger.ERROR) + ui.notifications.error('Unknown error. Unable to add show due to problem with show selection.') + self.redirect('/home/addShows/existingShows/') + indexer = int(series_pieces[1]) + indexer_id = int(series_pieces[3]) + show_name = series_pieces[4] + else: + # if no indexer was provided use the default indexer set in General settings + if not providedIndexer: + providedIndexer = sickbeard.INDEXER_DEFAULT + + indexer = int(providedIndexer) + indexer_id = int(whichSeries) + show_name = os.path.basename(os.path.normpath(fullShowPath)) + + # use the whole path if it's given, or else append the show name to the root dir to get the full show path + if fullShowPath: + show_dir = ek.ek(os.path.normpath, fullShowPath) + else: + show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) + + # blanket policy - if the dir exists you should have used 'add existing show' numbnuts + if ek.ek(os.path.isdir, show_dir) and not fullShowPath: + ui.notifications.error('Unable to add show', 'Folder ' + show_dir + ' exists already') + self.redirect('/home/addShows/existingShows/') + + # don't create show dir if config says not to + if sickbeard.ADD_SHOWS_WO_DIR: + logger.log(u'Skipping initial creation of ' + show_dir + ' due to config.ini setting') + else: + dir_exists = helpers.makeDir(show_dir) + if not dir_exists: + logger.log(u'Unable to create the folder ' + show_dir + ", can't add the show", logger.ERROR) + ui.notifications.error('Unable to add show', + 'Unable to create the folder ' + show_dir + ", can't add the show") + self.redirect('/home/') + else: + helpers.chmodAsParent(show_dir) + + # prepare the inputs for passing along + scene = config.checkbox_to_value(scene) + anime = config.checkbox_to_value(anime) + flatten_folders = config.checkbox_to_value(flatten_folders) + subtitles = config.checkbox_to_value(subtitles) + + if whitelist: + whitelist = short_group_names(whitelist) + if blacklist: + blacklist = short_group_names(blacklist) + + if not anyQualities: + anyQualities = [] + if not bestQualities: + bestQualities = [] + if type(anyQualities) != list: + anyQualities = [anyQualities] + if type(bestQualities) != list: + bestQualities = [bestQualities] + newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) + + # add the show + sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, int(defaultStatus), newQuality, + flatten_folders, indexerLang, subtitles, anime, + scene, None, blacklist, whitelist) # @UndefinedVariable + ui.notifications.message('Show added', 'Adding the specified show into ' + show_dir) + + return finishAddShow() + + def split_extra_show(self, extra_show): + if not extra_show: + return (None, None, None, None) + split_vals = extra_show.split('|') + if len(split_vals) < 4: + indexer = split_vals[0] + show_dir = split_vals[1] + return (indexer, show_dir, None, None) + indexer = split_vals[0] + show_dir = split_vals[1] + indexer_id = split_vals[2] + show_name = '|'.join(split_vals[3:]) + + return (indexer, show_dir, indexer_id, show_name) + + def addExistingShows(self, shows_to_add=None, promptForSettings=None): + """ + Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards + along to the newShow page. + """ + + # grab a list of other shows to add, if provided + if not shows_to_add: + shows_to_add = [] + elif type(shows_to_add) != list: + shows_to_add = [shows_to_add] + + shows_to_add = [urllib.unquote_plus(x) for x in shows_to_add] + + promptForSettings = config.checkbox_to_value(promptForSettings) + + indexer_id_given = [] + dirs_only = [] + # separate all the ones with Indexer IDs + for cur_dir in shows_to_add: + if '|' in cur_dir: + split_vals = cur_dir.split('|') + if len(split_vals) < 3: + dirs_only.append(cur_dir) + if not '|' in cur_dir: + dirs_only.append(cur_dir) + else: + indexer, show_dir, indexer_id, show_name = self.split_extra_show(cur_dir) + + if not show_dir or not indexer_id or not show_name: + continue + + indexer_id_given.append((int(indexer), show_dir, int(indexer_id), show_name)) + + + # if they want me to prompt for settings then I will just carry on to the newShow page + if promptForSettings and shows_to_add: + return self.newShow(shows_to_add[0], shows_to_add[1:]) + + # if they don't want me to prompt for settings then I can just add all the nfo shows now + num_added = 0 + for cur_show in indexer_id_given: + indexer, show_dir, indexer_id, show_name = cur_show + + if indexer is not None and indexer_id is not None: + # add the show + sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, + default_status=sickbeard.STATUS_DEFAULT, + quality=sickbeard.QUALITY_DEFAULT, + flatten_folders=sickbeard.FLATTEN_FOLDERS_DEFAULT, + subtitles=sickbeard.SUBTITLES_DEFAULT, + anime=sickbeard.ANIME_DEFAULT, + scene=sickbeard.SCENE_DEFAULT) + num_added += 1 + + if num_added: + ui.notifications.message('Shows Added', + 'Automatically added ' + str(num_added) + ' from their existing metadata files') + + # if we're done then go home + if not dirs_only: + self.redirect('/home/') + + # for the remaining shows we need to prompt for each one, so forward this on to the newShow page + return self.newShow(dirs_only[0], dirs_only[1:]) class Manage(MainHandler): + def ManageMenu(self): + manageMenu = [ + {'title': 'Backlog Overview', 'path': 'manage/backlogOverview/'}, + {'title': 'Manage Searches', 'path': 'manage/manageSearches/'}, + {'title': 'Episode Status Management', 'path': 'manage/episodeStatuses/'}, ] + + if sickbeard.USE_TORRENTS and sickbeard.TORRENT_METHOD != 'blackhole' \ + and (sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'https' + or not sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'http:'): + manageMenu.append({'title': 'Manage Torrents', 'path': 'manage/manageTorrents/'}) + + if sickbeard.USE_SUBTITLES: + manageMenu.append({'title': 'Missed Subtitle Management', 'path': 'manage/subtitleMissed/'}) + + if sickbeard.USE_FAILED_DOWNLOADS: + manageMenu.append({'title': 'Failed Downloads', 'path': 'manage/failedDownloads/'}) + + return manageMenu + def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="manage.tmpl") - t.submenu = ManageMenu() - return _munge(t) + t = PageTemplate(headers=self.request.headers, file='manage.tmpl') + t.submenu = self.ManageMenu() + return t.respond() def showEpisodeStatuses(self, indexer_id, whichStatus): status_list = [int(whichStatus)] @@ -681,18 +2358,18 @@ class Manage(MainHandler): myDB = db.DBConnection() cur_show_results = myDB.select( - "SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN (" + ','.join( - ['?'] * len(status_list)) + ")", [int(indexer_id)] + status_list) + 'SELECT season, episode, name FROM tv_episodes WHERE showid = ? AND season != 0 AND status IN (' + ','.join( + ['?'] * len(status_list)) + ')', [int(indexer_id)] + status_list) result = {} for cur_result in cur_show_results: - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) + cur_season = int(cur_result['season']) + cur_episode = int(cur_result['episode']) if cur_season not in result: result[cur_season] = {} - result[cur_season][cur_episode] = cur_result["name"] + result[cur_season][cur_episode] = cur_result['name'] return json.dumps(result) @@ -706,39 +2383,39 @@ class Manage(MainHandler): else: status_list = [] - t = PageTemplate(headers=self.request.headers, file="manage_episodeStatuses.tmpl") - t.submenu = ManageMenu() + t = PageTemplate(headers=self.request.headers, file='manage_episodeStatuses.tmpl') + t.submenu = self.ManageMenu() t.whichStatus = whichStatus # if we have no status then this is as far as we need to go if not status_list: - return _munge(t) + return t.respond() myDB = db.DBConnection() status_results = myDB.select( - "SELECT show_name, tv_shows.indexer_id as indexer_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN (" + ','.join( + 'SELECT show_name, tv_shows.indexer_id as indexer_id FROM tv_episodes, tv_shows WHERE tv_episodes.status IN (' + ','.join( ['?'] * len( - status_list)) + ") AND season != 0 AND tv_episodes.showid = tv_shows.indexer_id ORDER BY show_name", + status_list)) + ') AND season != 0 AND tv_episodes.showid = tv_shows.indexer_id ORDER BY show_name', status_list) ep_counts = {} show_names = {} sorted_show_ids = [] for cur_status_result in status_results: - cur_indexer_id = int(cur_status_result["indexer_id"]) + cur_indexer_id = int(cur_status_result['indexer_id']) if cur_indexer_id not in ep_counts: ep_counts[cur_indexer_id] = 1 else: ep_counts[cur_indexer_id] += 1 - show_names[cur_indexer_id] = cur_status_result["show_name"] + show_names[cur_indexer_id] = cur_status_result['show_name'] if cur_indexer_id not in sorted_show_ids: sorted_show_ids.append(cur_indexer_id) t.show_names = show_names t.ep_counts = ep_counts t.sorted_show_ids = sorted_show_ids - return _munge(t) + return t.respond() def changeEpisodeStatuses(self, oldStatus, newStatus, *args, **kwargs): @@ -764,19 +2441,19 @@ class Manage(MainHandler): myDB = db.DBConnection() for cur_indexer_id in to_change: - # get a list of all the eps we want to change if they just said "all" + # get a list of all the eps we want to change if they just said 'all' if 'all' in to_change[cur_indexer_id]: all_eps_results = myDB.select( - "SELECT season, episode FROM tv_episodes WHERE status IN (" + ','.join( - ['?'] * len(status_list)) + ") AND season != 0 AND showid = ?", + 'SELECT season, episode FROM tv_episodes WHERE status IN (' + ','.join( + ['?'] * len(status_list)) + ') AND season != 0 AND showid = ?', status_list + [cur_indexer_id]) - all_eps = [str(x["season"]) + 'x' + str(x["episode"]) for x in all_eps_results] + all_eps = [str(x['season']) + 'x' + str(x['episode']) for x in all_eps_results] to_change[cur_indexer_id] = all_eps Home(self.application, self.request).setStatus(cur_indexer_id, '|'.join(to_change[cur_indexer_id]), newStatus, direct=True) - redirect('/manage/episodeStatuses/') + self.redirect('/manage/episodeStatuses/') def showSubtitleMissed(self, indexer_id, whichSubs): myDB = db.DBConnection() @@ -787,14 +2464,14 @@ class Manage(MainHandler): result = {} for cur_result in cur_show_results: if whichSubs == 'all': - if len(set(cur_result["subtitles"].split(',')).intersection(set(subtitles.wantedLanguages()))) >= len( + if len(set(cur_result['subtitles'].split(',')).intersection(set(subtitles.wantedLanguages()))) >= len( subtitles.wantedLanguages()): continue - elif whichSubs in cur_result["subtitles"].split(','): + elif whichSubs in cur_result['subtitles'].split(','): continue - cur_season = int(cur_result["season"]) - cur_episode = int(cur_result["episode"]) + cur_season = int(cur_result['season']) + cur_episode = int(cur_result['episode']) if cur_season not in result: result[cur_season] = {} @@ -802,22 +2479,22 @@ class Manage(MainHandler): if cur_episode not in result[cur_season]: result[cur_season][cur_episode] = {} - result[cur_season][cur_episode]["name"] = cur_result["name"] + result[cur_season][cur_episode]['name'] = cur_result['name'] - result[cur_season][cur_episode]["subtitles"] = ",".join( - subliminal.language.Language(subtitle).alpha2 for subtitle in cur_result["subtitles"].split(',')) if not \ - cur_result["subtitles"] == '' else '' + result[cur_season][cur_episode]['subtitles'] = ','.join( + subliminal.language.Language(subtitle).alpha2 for subtitle in cur_result['subtitles'].split(',')) if not \ + cur_result['subtitles'] == '' else '' return json.dumps(result) def subtitleMissed(self, whichSubs=None): - t = PageTemplate(headers=self.request.headers, file="manage_subtitleMissed.tmpl") - t.submenu = ManageMenu() + t = PageTemplate(headers=self.request.headers, file='manage_subtitleMissed.tmpl') + t.submenu = self.ManageMenu() t.whichSubs = whichSubs if not whichSubs: - return _munge(t) + return t.respond() myDB = db.DBConnection() status_results = myDB.select( @@ -828,26 +2505,26 @@ class Manage(MainHandler): sorted_show_ids = [] for cur_status_result in status_results: if whichSubs == 'all': - if len(set(cur_status_result["subtitles"].split(',')).intersection( + if len(set(cur_status_result['subtitles'].split(',')).intersection( set(subtitles.wantedLanguages()))) >= len(subtitles.wantedLanguages()): continue - elif whichSubs in cur_status_result["subtitles"].split(','): + elif whichSubs in cur_status_result['subtitles'].split(','): continue - cur_indexer_id = int(cur_status_result["indexer_id"]) + cur_indexer_id = int(cur_status_result['indexer_id']) if cur_indexer_id not in ep_counts: ep_counts[cur_indexer_id] = 1 else: ep_counts[cur_indexer_id] += 1 - show_names[cur_indexer_id] = cur_status_result["show_name"] + show_names[cur_indexer_id] = cur_status_result['show_name'] if cur_indexer_id not in sorted_show_ids: sorted_show_ids.append(cur_indexer_id) t.show_names = show_names t.ep_counts = ep_counts t.sorted_show_ids = sorted_show_ids - return _munge(t) + return t.respond() def downloadSubtitleMissed(self, *args, **kwargs): @@ -867,13 +2544,13 @@ class Manage(MainHandler): to_download[indexer_id].append(what) for cur_indexer_id in to_download: - # get a list of all the eps we want to download subtitles if they just said "all" + # get a list of all the eps we want to download subtitles if they just said 'all' if 'all' in to_download[cur_indexer_id]: myDB = db.DBConnection() all_eps_results = myDB.select( "SELECT season, episode FROM tv_episodes WHERE status LIKE '%4' AND season != 0 AND showid = ?", [cur_indexer_id]) - to_download[cur_indexer_id] = [str(x["season"]) + 'x' + str(x["episode"]) for x in all_eps_results] + to_download[cur_indexer_id] = [str(x['season']) + 'x' + str(x['episode']) for x in all_eps_results] for epResult in to_download[cur_indexer_id]: season, episode = epResult.split('x') @@ -881,7 +2558,7 @@ class Manage(MainHandler): show = sickbeard.helpers.findCertainShow(sickbeard.showList, int(cur_indexer_id)) subtitles = show.getEpisode(int(season), int(episode)).downloadSubtitles() - redirect('/manage/subtitleMissed/') + self.redirect('/manage/subtitleMissed/') def backlogShow(self, indexer_id): @@ -890,12 +2567,12 @@ class Manage(MainHandler): if show_obj: sickbeard.backlogSearchScheduler.action.searchBacklog([show_obj]) # @UndefinedVariable - redirect("/manage/backlogOverview/") + self.redirect('/manage/backlogOverview/') def backlogOverview(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="manage_backlogOverview.tmpl") - t.submenu = ManageMenu() + t = PageTemplate(headers=self.request.headers, file='manage_backlogOverview.tmpl') + t.submenu = self.ManageMenu() showCounts = {} showCats = {} @@ -914,13 +2591,13 @@ class Manage(MainHandler): epCounts[Overview.SNATCHED] = 0 sqlResults = myDB.select( - "SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", + 'SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC', [curShow.indexerid]) for curResult in sqlResults: - curEpCat = curShow.getOverview(int(curResult["status"])) + curEpCat = curShow.getOverview(int(curResult['status'])) if curEpCat: - epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat + epCats[str(curResult['season']) + 'x' + str(curResult['episode'])] = curEpCat epCounts[curEpCat] += 1 showCounts[curShow.indexerid] = epCounts @@ -931,17 +2608,17 @@ class Manage(MainHandler): t.showCats = showCats t.showSQLResults = showSQLResults - return _munge(t) + return t.respond() def massEdit(self, toEdit=None): - t = PageTemplate(headers=self.request.headers, file="manage_massEdit.tmpl") - t.submenu = ManageMenu() + t = PageTemplate(headers=self.request.headers, file='manage_massEdit.tmpl') + t.submenu = self.ManageMenu() if not toEdit: - redirect("/manage/") + self.redirect('/manage/') - showIDs = toEdit.split("|") + showIDs = toEdit.split('|') showList = [] for curID in showIDs: curID = int(curID) @@ -1054,7 +2731,7 @@ class Manage(MainHandler): t.air_by_date_value = last_air_by_date if air_by_date_all_same else None t.root_dir_list = root_dir_list - return _munge(t) + return t.respond() def massEditSubmit(self, archive_firstmatch=None, paused=None, anime=None, sports=None, scene=None, flatten_folders=None, quality_preset=False, @@ -1069,7 +2746,7 @@ class Manage(MainHandler): end_dir = kwargs['new_root_dir_' + which_index] dir_map[kwargs[cur_arg]] = end_dir - showIDs = toEdit.split("|") + showIDs = toEdit.split('|') errors = [] for curShow in showIDs: curErrors = [] @@ -1082,7 +2759,7 @@ class Manage(MainHandler): if cur_root_dir in dir_map and cur_root_dir != dir_map[cur_root_dir]: new_show_dir = ek.ek(os.path.join, dir_map[cur_root_dir], cur_show_dir) logger.log( - u"For show " + showObj.name + " changing dir from " + showObj._location + " to " + new_show_dir) + u'For show ' + showObj.name + ' changing dir from ' + showObj._location + ' to ' + new_show_dir) else: new_show_dir = showObj._location @@ -1150,15 +2827,15 @@ class Manage(MainHandler): directCall=True) if curErrors: - logger.log(u"Errors: " + str(curErrors), logger.ERROR) + logger.log(u'Errors: ' + str(curErrors), logger.ERROR) errors.append('%s:\n
    ' % showObj.name + ' '.join( - ['
  • %s
  • ' % error for error in curErrors]) + "
") + ['
  • %s
  • ' % error for error in curErrors]) + '') if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - " ".join(errors)) + ui.notifications.error('%d error%s while saving changes:' % (len(errors), '' if len(errors) == 1 else 's'), + ' '.join(errors)) - redirect("/manage/") + self.redirect('/manage/') def massUpdate(self, toUpdate=None, toRefresh=None, toRename=None, toDelete=None, toRemove=None, toMetadata=None, toSubtitle=None): @@ -1228,7 +2905,7 @@ class Manage(MainHandler): sickbeard.showQueueScheduler.action.updateShow(showObj, True) # @UndefinedVariable updates.append(showObj.name) except exceptions.CantUpdateException, e: - errors.append("Unable to update show " + showObj.name + ": " + ex(e)) + errors.append('Unable to update show ' + showObj.name + ': ' + ex(e)) # don't bother refreshing shows that were updated anyway if curShowID in toRefresh and curShowID not in toUpdate: @@ -1236,7 +2913,7 @@ class Manage(MainHandler): sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable refreshes.append(showObj.name) except exceptions.CantRefreshException, e: - errors.append("Unable to refresh show " + showObj.name + ": " + ex(e)) + errors.append('Unable to refresh show ' + showObj.name + ': ' + ex(e)) if curShowID in toRename: sickbeard.showQueueScheduler.action.renameShowEpisodes(showObj) # @UndefinedVariable @@ -1247,42 +2924,42 @@ class Manage(MainHandler): subtitles.append(showObj.name) if len(errors) > 0: - ui.notifications.error("Errors encountered", + ui.notifications.error('Errors encountered', '
    \n'.join(errors)) - messageDetail = "" + messageDetail = '' if len(updates) > 0: - messageDetail += "
    Updates
    • " - messageDetail += "
    • ".join(updates) - messageDetail += "
    " + messageDetail += '
    Updates
    • ' + messageDetail += '
    • '.join(updates) + messageDetail += '
    ' if len(refreshes) > 0: - messageDetail += "
    Refreshes
    • " - messageDetail += "
    • ".join(refreshes) - messageDetail += "
    " + messageDetail += '
    Refreshes
    • ' + messageDetail += '
    • '.join(refreshes) + messageDetail += '
    ' if len(renames) > 0: - messageDetail += "
    Renames
    • " - messageDetail += "
    • ".join(renames) - messageDetail += "
    " + messageDetail += '
    Renames
    • ' + messageDetail += '
    • '.join(renames) + messageDetail += '
    ' if len(subtitles) > 0: - messageDetail += "
    Subtitles
    • " - messageDetail += "
    • ".join(subtitles) - messageDetail += "
    " + messageDetail += '
    Subtitles
    • ' + messageDetail += '
    • '.join(subtitles) + messageDetail += '
    ' if len(updates + refreshes + renames + subtitles) > 0: - ui.notifications.message("The following actions were queued:", + ui.notifications.message('The following actions were queued:', messageDetail) - redirect("/manage/") + self.redirect('/manage/') def manageTorrents(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="manage_torrents.tmpl") + t = PageTemplate(headers=self.request.headers, file='manage_torrents.tmpl') t.info_download_station = '' - t.submenu = ManageMenu() + t.submenu = self.ManageMenu() if re.search('localhost', sickbeard.TORRENT_HOST): @@ -1301,44 +2978,103 @@ class Manage(MainHandler): else: t.info_download_station = '

    To have a better experience please set the Download Station alias as download, you can check this setting in the Synology DSM Control Panel > Application Portal. Make sure you allow DSM to be embedded with iFrames too in Control Panel > DSM Settings > Security.


    There is more information about this available here.


    ' - return _munge(t) + return t.respond() def failedDownloads(self, limit=100, toRemove=None): myDB = db.DBConnection('failed.db') - if limit == "0": - sqlResults = myDB.select("SELECT * FROM failed") + if limit == '0': + sqlResults = myDB.select('SELECT * FROM failed') else: - sqlResults = myDB.select("SELECT * FROM failed LIMIT ?", [limit]) + sqlResults = myDB.select('SELECT * FROM failed LIMIT ?', [limit]) - toRemove = toRemove.split("|") if toRemove is not None else [] + toRemove = toRemove.split('|') if toRemove is not None else [] for release in toRemove: myDB.action('DELETE FROM failed WHERE release = ?', [release]) if toRemove: - redirect('/manage/failedDownloads/') + self.redirect('/manage/failedDownloads/') - t = PageTemplate(headers=self.request.headers, file="manage_failedDownloads.tmpl") + t = PageTemplate(headers=self.request.headers, file='manage_failedDownloads.tmpl') t.failedResults = sqlResults t.limit = limit - t.submenu = ManageMenu() + t.submenu = self.ManageMenu() - return _munge(t) + return t.respond() + + +class ManageSearches(Manage): + def index(self, *args, **kwargs): + t = PageTemplate(headers=self.request.headers, file='manage_manageSearches.tmpl') + # t.backlogPI = sickbeard.backlogSearchScheduler.action.getProgressIndicator() + t.backlogPaused = sickbeard.searchQueueScheduler.action.is_backlog_paused() # @UndefinedVariable + t.backlogRunning = sickbeard.searchQueueScheduler.action.is_backlog_in_progress() # @UndefinedVariable + t.recentSearchStatus = sickbeard.recentSearchScheduler.action.amActive # @UndefinedVariable + t.findPropersStatus = sickbeard.properFinderScheduler.action.amActive # @UndefinedVariable + t.queueLength = sickbeard.searchQueueScheduler.action.queue_length() + + t.submenu = self.ManageMenu() + + return t.respond() + + def forceVersionCheck(self, *args, **kwargs): + # force a check to see if there is a new version + if sickbeard.versionCheckScheduler.action.check_for_new_version(force=True): + logger.log(u'Forcing version check') + + self.redirect('/home/') + + def forceBacklog(self, *args, **kwargs): + # force it to run the next time it looks + result = sickbeard.backlogSearchScheduler.forceRun() + if result: + logger.log(u'Backlog search forced') + ui.notifications.message('Backlog search started') + + self.redirect('/manage/manageSearches/') + + def forceSearch(self, *args, **kwargs): + + # force it to run the next time it looks + result = sickbeard.recentSearchScheduler.forceRun() + if result: + logger.log(u'Recent search forced') + ui.notifications.message('Recent search started') + + self.redirect('/manage/manageSearches/') + + def forceFindPropers(self, *args, **kwargs): + + # force it to run the next time it looks + result = sickbeard.properFinderScheduler.forceRun() + if result: + logger.log(u'Find propers search forced') + ui.notifications.message('Find propers search started') + + self.redirect('/manage/manageSearches/') + + def pauseBacklog(self, paused=None): + if paused == '1': + sickbeard.searchQueueScheduler.action.pause_backlog() # @UndefinedVariable + else: + sickbeard.searchQueueScheduler.action.unpause_backlog() # @UndefinedVariable + + self.redirect('/manage/manageSearches/') class History(MainHandler): def index(self, limit=100): - # sqlResults = myDB.select("SELECT h.*, show_name, name FROM history h, tv_shows s, tv_episodes e WHERE h.showid=s.indexer_id AND h.showid=e.showid AND h.season=e.season AND h.episode=e.episode ORDER BY date DESC LIMIT "+str(numPerPage*(p-1))+", "+str(numPerPage)) + # sqlResults = myDB.select('SELECT h.*, show_name, name FROM history h, tv_shows s, tv_episodes e WHERE h.showid=s.indexer_id AND h.showid=e.showid AND h.season=e.season AND h.episode=e.episode ORDER BY date DESC LIMIT '+str(numPerPage*(p-1))+', '+str(numPerPage)) myDB = db.DBConnection() - if limit == "0": + if limit == '0': sqlResults = myDB.select( - "SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.indexer_id ORDER BY date DESC") + 'SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.indexer_id ORDER BY date DESC') else: sqlResults = myDB.select( - "SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.indexer_id ORDER BY date DESC LIMIT ?", + 'SELECT h.*, show_name FROM history h, tv_shows s WHERE h.showid=s.indexer_id ORDER BY date DESC LIMIT ?', [limit]) history = {'show_id': 0, 'season': 0, 'episode': 0, 'quality': 0, @@ -1388,7 +3124,7 @@ class History(MainHandler): history['actions'].append(action) history['actions'].sort(key=lambda x: x['time'], reverse=True) - t = PageTemplate(headers=self.request.headers, file="history.tmpl") + t = PageTemplate(headers=self.request.headers, file='history.tmpl') t.historyResults = sqlResults t.compactResults = compact t.limit = limit @@ -1397,45 +3133,52 @@ class History(MainHandler): {'title': 'Trim History', 'path': 'history/trimHistory'}, ] - return _munge(t) - + return t.respond() def clearHistory(self, *args, **kwargs): myDB = db.DBConnection() - myDB.action("DELETE FROM history WHERE 1=1") + myDB.action('DELETE FROM history WHERE 1=1') ui.notifications.message('History cleared') - redirect("/history/") - + self.redirect('/history/') def trimHistory(self, *args, **kwargs): myDB = db.DBConnection() - myDB.action("DELETE FROM history WHERE date < " + str( + myDB.action('DELETE FROM history WHERE date < ' + str( (datetime.datetime.today() - datetime.timedelta(days=30)).strftime(history.dateFormat))) ui.notifications.message('Removed history entries greater than 30 days old') - redirect("/history/") + self.redirect('/history/') -ConfigMenu = [ - {'title': 'General', 'path': 'config/general/'}, - {'title': 'Search Settings', 'path': 'config/search/'}, - {'title': 'Search Providers', 'path': 'config/providers/'}, - {'title': 'Subtitles Settings', 'path': 'config/subtitles/'}, - {'title': 'Post Processing', 'path': 'config/postProcessing/'}, - {'title': 'Notifications', 'path': 'config/notifications/'}, - {'title': 'Anime', 'path': 'config/anime/'}, -] +class Config(MainHandler): + @staticmethod + def ConfigMenu(): + return [ + {'title': 'General', 'path': 'config/general/'}, + {'title': 'Search Settings', 'path': 'config/search/'}, + {'title': 'Search Providers', 'path': 'config/providers/'}, + {'title': 'Subtitles Settings', 'path': 'config/subtitles/'}, + {'title': 'Post Processing', 'path': 'config/postProcessing/'}, + {'title': 'Notifications', 'path': 'config/notifications/'}, + {'title': 'Anime', 'path': 'config/anime/'}, + ] + + def index(self, *args, **kwargs): + t = PageTemplate(headers=self.request.headers, file='config.tmpl') + t.submenu = self.ConfigMenu + + return t.respond() -class ConfigGeneral(MainHandler): +class ConfigGeneral(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_general.tmpl") - t.submenu = ConfigMenu - return _munge(t) + t = PageTemplate(headers=self.request.headers, file='config_general.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def saveRootDirs(self, rootDirString=None): sickbeard.ROOT_DIRS = rootDirString @@ -1486,7 +3229,7 @@ class ConfigGeneral(MainHandler): m.update(r) # Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b - logger.log(u"New API generated") + logger.log(u'New API generated') return m.hexdigest() def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, @@ -1546,12 +3289,12 @@ class ConfigGeneral(MainHandler): if time_preset: sickbeard.TIME_PRESET_W_SECONDS = time_preset - sickbeard.TIME_PRESET = sickbeard.TIME_PRESET_W_SECONDS.replace(u":%S", u"") + sickbeard.TIME_PRESET = sickbeard.TIME_PRESET_W_SECONDS.replace(u':%S', u'') sickbeard.TIMEZONE_DISPLAY = timezone_display if not config.change_LOG_DIR(log_dir, web_log): - results += ["Unable to create directory " + os.path.normpath(log_dir) + ", log directory not changed."] + results += ['Unable to create directory ' + os.path.normpath(log_dir) + ', log directory not changed.'] sickbeard.USE_API = config.checkbox_to_value(use_api) sickbeard.API_KEY = api_key @@ -1560,11 +3303,11 @@ class ConfigGeneral(MainHandler): if not config.change_HTTPS_CERT(https_cert): results += [ - "Unable to create directory " + os.path.normpath(https_cert) + ", https cert directory not changed."] + 'Unable to create directory ' + os.path.normpath(https_cert) + ', https cert directory not changed.'] if not config.change_HTTPS_KEY(https_key): results += [ - "Unable to create directory " + os.path.normpath(https_key) + ", https key directory not changed."] + 'Unable to create directory ' + os.path.normpath(https_key) + ', https key directory not changed.'] sickbeard.HANDLE_REVERSE_PROXY = config.checkbox_to_value(handle_reverse_proxy) @@ -1580,15 +3323,15 @@ class ConfigGeneral(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/general/") + self.redirect('/config/general/') -class ConfigSearch(MainHandler): +class ConfigSearch(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_search.tmpl") - t.submenu = ConfigMenu - return _munge(t) + t = PageTemplate(headers=self.request.headers, file='config_search.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def saveSearch(self, use_nzbs=None, use_torrents=None, nzb_dir=None, sab_username=None, sab_password=None, sab_apikey=None, sab_category=None, sab_host=None, nzbget_username=None, nzbget_password=None, @@ -1603,10 +3346,10 @@ class ConfigSearch(MainHandler): results = [] if not config.change_NZB_DIR(nzb_dir): - results += ["Unable to create directory " + os.path.normpath(nzb_dir) + ", dir not changed."] + results += ['Unable to create directory ' + os.path.normpath(nzb_dir) + ', dir not changed.'] if not config.change_TORRENT_DIR(torrent_dir): - results += ["Unable to create directory " + os.path.normpath(torrent_dir) + ", dir not changed."] + results += ['Unable to create directory ' + os.path.normpath(torrent_dir) + ', dir not changed.'] config.change_RECENTSEARCH_FREQUENCY(recentsearch_frequency) @@ -1620,8 +3363,8 @@ class ConfigSearch(MainHandler): sickbeard.TORRENT_METHOD = torrent_method sickbeard.USENET_RETENTION = config.to_int(usenet_retention, default=500) - sickbeard.IGNORE_WORDS = ignore_words if ignore_words else "" - sickbeard.REQUIRE_WORDS = require_words if require_words else "" + sickbeard.IGNORE_WORDS = ignore_words if ignore_words else '' + sickbeard.REQUIRE_WORDS = require_words if require_words else '' sickbeard.DOWNLOAD_PROPERS = config.checkbox_to_value(download_propers) sickbeard.CHECK_PROPERS_INTERVAL = check_propers_interval @@ -1661,15 +3404,15 @@ class ConfigSearch(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/search/") + self.redirect('/config/search/') -class ConfigPostProcessing(MainHandler): +class ConfigPostProcessing(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_postProcessing.tmpl") - t.submenu = ConfigMenu - return _munge(t) + t = PageTemplate(headers=self.request.headers, file='config_postProcessing.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def savePostProcessing(self, naming_pattern=None, naming_multi_ep=None, xbmc_data=None, xbmc_12plus_data=None, mediabrowser_data=None, sony_ps3_data=None, @@ -1687,7 +3430,7 @@ class ConfigPostProcessing(MainHandler): results = [] if not config.change_TV_DOWNLOAD_DIR(tv_download_dir): - results += ["Unable to create directory " + os.path.normpath(tv_download_dir) + ", dir not changed."] + results += ['Unable to create directory ' + os.path.normpath(tv_download_dir) + ', dir not changed.'] sickbeard.PROCESS_AUTOMATICALLY = config.checkbox_to_value(process_automatically) config.change_AUTOPOSTPROCESSER_FREQUENCY(autopostprocesser_frequency) @@ -1702,7 +3445,7 @@ class ConfigPostProcessing(MainHandler): sickbeard.UNPACK = config.checkbox_to_value(unpack) else: sickbeard.UNPACK = 0 - results.append("Unpacking Not Supported, disabling unpack setting") + results.append('Unpacking Not Supported, disabling unpack setting') else: sickbeard.UNPACK = config.checkbox_to_value(unpack) @@ -1738,39 +3481,39 @@ class ConfigPostProcessing(MainHandler): sickbeard.metadata_provider_dict['TIVO'].set_config(sickbeard.METADATA_TIVO) sickbeard.metadata_provider_dict['Mede8er'].set_config(sickbeard.METADATA_MEDE8ER) - if self.isNamingValid(naming_pattern, naming_multi_ep, anime_type=naming_anime) != "invalid": + if self.isNamingValid(naming_pattern, naming_multi_ep, anime_type=naming_anime) != 'invalid': sickbeard.NAMING_PATTERN = naming_pattern sickbeard.NAMING_MULTI_EP = int(naming_multi_ep) sickbeard.NAMING_ANIME = int(naming_anime) sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() else: if int(naming_anime) in [1, 2]: - results.append("You tried saving an invalid anime naming config, not saving your naming settings") + results.append('You tried saving an invalid anime naming config, not saving your naming settings') else: - results.append("You tried saving an invalid naming config, not saving your naming settings") + results.append('You tried saving an invalid naming config, not saving your naming settings') - if self.isNamingValid(naming_anime_pattern, naming_anime_multi_ep, anime_type=naming_anime) != "invalid": + if self.isNamingValid(naming_anime_pattern, naming_anime_multi_ep, anime_type=naming_anime) != 'invalid': sickbeard.NAMING_ANIME_PATTERN = naming_anime_pattern sickbeard.NAMING_ANIME_MULTI_EP = int(naming_anime_multi_ep) sickbeard.NAMING_ANIME = int(naming_anime) sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders() else: if int(naming_anime) in [1, 2]: - results.append("You tried saving an invalid anime naming config, not saving your naming settings") + results.append('You tried saving an invalid anime naming config, not saving your naming settings') else: - results.append("You tried saving an invalid naming config, not saving your naming settings") + results.append('You tried saving an invalid naming config, not saving your naming settings') - if self.isNamingValid(naming_abd_pattern, None, abd=True) != "invalid": + if self.isNamingValid(naming_abd_pattern, None, abd=True) != 'invalid': sickbeard.NAMING_ABD_PATTERN = naming_abd_pattern else: results.append( - "You tried saving an invalid air-by-date naming config, not saving your air-by-date settings") + 'You tried saving an invalid air-by-date naming config, not saving your air-by-date settings') - if self.isNamingValid(naming_sports_pattern, None, sports=True) != "invalid": + if self.isNamingValid(naming_sports_pattern, None, sports=True) != 'invalid': sickbeard.NAMING_SPORTS_PATTERN = naming_sports_pattern else: results.append( - "You tried saving an invalid sports naming config, not saving your sports settings") + 'You tried saving an invalid sports naming config, not saving your sports settings') sickbeard.save_config() @@ -1782,7 +3525,7 @@ class ConfigPostProcessing(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/postProcessing/") + self.redirect('/config/postProcessing/') def testNaming(self, pattern=None, multi=None, abd=False, sports=False, anime_type=None): @@ -1800,7 +3543,7 @@ class ConfigPostProcessing(MainHandler): def isNamingValid(self, pattern=None, multi=None, abd=False, sports=False, anime_type=None): if pattern is None: - return "invalid" + return 'invalid' if multi is not None: multi = int(multi) @@ -1826,11 +3569,11 @@ class ConfigPostProcessing(MainHandler): require_season_folders = naming.check_force_season_folders(pattern, multi, anime_type) if is_valid and not require_season_folders: - return "valid" + return 'valid' elif is_valid and require_season_folders: - return "seasonfolders" + return 'seasonfolders' else: - return "invalid" + return 'invalid' def isRarSupported(self, *args, **kwargs): """ @@ -1850,11 +3593,11 @@ class ConfigPostProcessing(MainHandler): return 'not supported' -class ConfigProviders(MainHandler): +class ConfigProviders(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_providers.tmpl") - t.submenu = ConfigMenu - return _munge(t) + t = PageTemplate(headers=self.request.headers, file='config_providers.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def canAddNewznabProvider(self, name): @@ -1902,17 +3645,17 @@ class ConfigProviders(MainHandler): Using the default url/api?cat http://yournewznaburl.com/api?t=caps&apikey=yourapikey ''' - error = "" + error = '' success = False if not name: - error += "\nNo Provider Name specified" + error += '\nNo Provider Name specified' if not url: - error += "\nNo Provider Url specified" + error += '\nNo Provider Url specified' if not key: - error += "\nNo Provider Api key specified" + error += '\nNo Provider Api key specified' - if error <> "": + if error <> '': return json.dumps({'success' : False, 'error': error}) #Get list with Newznabproviders @@ -2279,15 +4022,14 @@ class ConfigProviders(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/providers/") + self.redirect('/config/providers/') -class ConfigNotifications(MainHandler): +class ConfigNotifications(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_notifications.tmpl") - t.submenu = ConfigMenu - return _munge(t) - + t = PageTemplate(headers=self.request.headers, file='config_notifications.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def saveNotifications(self, use_xbmc=None, xbmc_always_on=None, xbmc_notify_onsnatch=None, xbmc_notify_ondownload=None, @@ -2481,15 +4223,14 @@ class ConfigNotifications(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/notifications/") + self.redirect('/config/notifications/') -class ConfigSubtitles(MainHandler): +class ConfigSubtitles(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_subtitles.tmpl") - t.submenu = ConfigMenu - return _munge(t) - + t = PageTemplate(headers=self.request.headers, file='config_subtitles.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def saveSubtitles(self, use_subtitles=None, subtitles_plugins=None, subtitles_languages=None, subtitles_dir=None, service_order=None, subtitles_history=None, subtitles_finder_frequency=None): @@ -2498,13 +4239,13 @@ class ConfigSubtitles(MainHandler): if subtitles_finder_frequency == '' or subtitles_finder_frequency is None: subtitles_finder_frequency = 1 - if use_subtitles == "on" and not sickbeard.subtitlesFinderScheduler.isAlive(): + if use_subtitles == 'on' and not sickbeard.subtitlesFinderScheduler.isAlive(): sickbeard.subtitlesFinderScheduler.silent = False sickbeard.subtitlesFinderScheduler.start() else: sickbeard.subtitlesFinderScheduler.stop.set() sickbeard.subtitlesFinderScheduler.silent = True - logger.log(u"Waiting for the SUBTITLESFINDER thread to exit") + logger.log(u'Waiting for the SUBTITLESFINDER thread to exit') try: sickbeard.subtitlesFinderScheduler.join(5) except: @@ -2539,15 +4280,15 @@ class ConfigSubtitles(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/subtitles/") + self.redirect('/config/subtitles/') -class ConfigAnime(MainHandler): +class ConfigAnime(Config): def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config_anime.tmpl") - t.submenu = ConfigMenu - return _munge(t) + t = PageTemplate(headers=self.request.headers, file='config_anime.tmpl') + t.submenu = self.ConfigMenu + return t.respond() def saveAnime(self, use_anidb=None, anidb_username=None, anidb_password=None, anidb_use_mylist=None, split_home=None, anime_treat_as_hdtv=None): @@ -2571,599 +4312,48 @@ class ConfigAnime(MainHandler): else: ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) - redirect("/config/anime/") + self.redirect('/config/anime/') -class Config(MainHandler): - def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="config.tmpl") - t.submenu = ConfigMenu - return _munge(t) +class UI(MainHandler): + def add_message(self): + ui.notifications.message('Test 1', 'This is test number 1') + ui.notifications.error('Test 2', 'This is test number 2') - # map class names to urls - general = ConfigGeneral - search = ConfigSearch - providers = ConfigProviders - subtitles = ConfigSubtitles - postProcessing = ConfigPostProcessing - notifications = ConfigNotifications - anime = ConfigAnime + return 'ok' + def get_messages(self): + messages = {} + cur_notification_num = 1 + for cur_notification in ui.notifications.get_notifications(self.request.remote_ip): + messages['notification-' + str(cur_notification_num)] = {'title': cur_notification.title, + 'message': cur_notification.message, + 'type': cur_notification.type} + cur_notification_num += 1 -def haveXBMC(): - return sickbeard.USE_XBMC and sickbeard.XBMC_UPDATE_LIBRARY - - -def havePLEX(): - return sickbeard.USE_PLEX and sickbeard.PLEX_UPDATE_LIBRARY - - -def haveTORRENT(): - if sickbeard.USE_TORRENTS and sickbeard.TORRENT_METHOD != 'blackhole' \ - and (sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'https' - or not sickbeard.ENABLE_HTTPS and sickbeard.TORRENT_HOST[:5] == 'http:'): - return True - else: - return False - - -def HomeMenu(): - return [ - {'title': 'Add Shows', 'path': 'home/addShows/', }, - {'title': 'Manual Post-Processing', 'path': 'home/postprocess/'}, - {'title': 'Update XBMC', 'path': 'home/updateXBMC/', 'requires': haveXBMC}, - {'title': 'Update Plex', 'path': 'home/updatePLEX/', 'requires': havePLEX}, - {'title': 'Manage Torrents', 'path': 'manage/manageTorrents', 'requires': haveTORRENT}, - {'title': 'Restart', 'path': 'home/restart/?pid=' + str(sickbeard.PID), 'confirm': True}, - {'title': 'Shutdown', 'path': 'home/shutdown/?pid=' + str(sickbeard.PID), 'confirm': True}, - ] - - -class HomePostProcess(MainHandler): - def index(self, *args, **kwargs): - - t = PageTemplate(headers=self.request.headers, file="home_postprocess.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - def processEpisode(self, dir=None, nzbName=None, jobName=None, quiet=None, process_method=None, force=None, - is_priority=None, failed="0", type="auto", *args, **kwargs): - - if failed == "0": - failed = False - else: - failed = True - - if force in ["on", "1"]: - force = True - else: - force = False - - if is_priority in ["on", "1"]: - is_priority = True - else: - is_priority = False - - if not dir: - redirect("/home/postprocess/") - else: - result = processTV.processDir(dir, nzbName, process_method=process_method, force=force, - is_priority=is_priority, failed=failed, type=type) - if quiet is not None and int(quiet) == 1: - return result - - result = result.replace("\n", "
    \n") - return self._genericMessage("Postprocessing results", result) - - -class NewHomeAddShows(MainHandler): - def index(self, *args, **kwargs): - - t = PageTemplate(headers=self.request.headers, file="home_addShows.tmpl") - t.submenu = HomeMenu() - return _munge(t) - - def getIndexerLanguages(self, *args, **kwargs): - result = sickbeard.indexerApi().config['valid_languages'] - - # Make sure list is sorted alphabetically but 'en' is in front - if 'en' in result: - del result[result.index('en')] - result.sort() - result.insert(0, 'en') - - return json.dumps({'results': result}) - - def sanitizeFileName(self, name): - return helpers.sanitizeFileName(name) - - def searchIndexersForShowName(self, search_term, lang="en", indexer=None): - if not lang or lang == 'null': - lang = "en" - - search_term = search_term.encode('utf-8') - - results = {} - final_results = [] - - # Query Indexers for each search term and build the list of results - for indexer in sickbeard.indexerApi().indexers if not int(indexer) else [int(indexer)]: - lINDEXER_API_PARMS = sickbeard.indexerApi(indexer).api_params.copy() - lINDEXER_API_PARMS['language'] = lang - lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsListUI - t = sickbeard.indexerApi(indexer).indexer(**lINDEXER_API_PARMS) - - logger.log("Searching for Show with searchterm: %s on Indexer: %s" % ( - search_term, sickbeard.indexerApi(indexer).name), logger.DEBUG) - try: - # add search results - results.setdefault(indexer, []).extend(t[search_term]) - except Exception, e: - continue - - map(final_results.extend, - ([[sickbeard.indexerApi(id).name, id, sickbeard.indexerApi(id).config["show_url"], int(show['id']), - show['seriesname'], show['firstaired']] for show in shows] for id, shows in results.items())) - - lang_id = sickbeard.indexerApi().config['langabbv_to_id'][lang] - return json.dumps({'results': final_results, 'langid': lang_id}) - - def massAddTable(self, rootDir=None): - t = PageTemplate(headers=self.request.headers, file="home_massAddTable.tmpl") - t.submenu = HomeMenu() - - if not rootDir: - return "No folders selected." - elif type(rootDir) != list: - root_dirs = [rootDir] - else: - root_dirs = rootDir - - root_dirs = [urllib.unquote_plus(x) for x in root_dirs] - - if sickbeard.ROOT_DIRS: - default_index = int(sickbeard.ROOT_DIRS.split('|')[0]) - else: - default_index = 0 - - if len(root_dirs) > default_index: - tmp = root_dirs[default_index] - if tmp in root_dirs: - root_dirs.remove(tmp) - root_dirs = [tmp] + root_dirs - - dir_list = [] - - myDB = db.DBConnection() - for root_dir in root_dirs: - try: - file_list = ek.ek(os.listdir, root_dir) - except: - continue - - for cur_file in file_list: - - cur_path = ek.ek(os.path.normpath, ek.ek(os.path.join, root_dir, cur_file)) - if not ek.ek(os.path.isdir, cur_path): - continue - - cur_dir = { - 'dir': cur_path, - 'display_dir': '' + ek.ek(os.path.dirname, cur_path) + os.sep + '' + ek.ek( - os.path.basename, - cur_path), - } - - # see if the folder is in XBMC already - dirResults = myDB.select("SELECT * FROM tv_shows WHERE location = ?", [cur_path]) - - if dirResults: - cur_dir['added_already'] = True - else: - cur_dir['added_already'] = False - - dir_list.append(cur_dir) - - indexer_id = show_name = indexer = None - for cur_provider in sickbeard.metadata_provider_dict.values(): - if indexer_id and show_name: - continue - - (indexer_id, show_name, indexer) = cur_provider.retrieveShowMetadata(cur_path) - - # default to TVDB if indexer was not detected - if show_name and not (indexer or indexer_id): - (sn, idx, id) = helpers.searchIndexerForShowID(show_name, indexer, indexer_id) - - # set indexer and indexer_id from found info - if not indexer and idx: - indexer = idx - - if not indexer_id and id: - indexer_id = id - - cur_dir['existing_info'] = (indexer_id, show_name, indexer) - - if indexer_id and helpers.findCertainShow(sickbeard.showList, indexer_id): - cur_dir['added_already'] = True - - t.dirList = dir_list - - return _munge(t) - - def newShow(self, show_to_add=None, other_shows=None, use_show_name=None): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - self.set_header('Cache-Control', 'no-cache, no-store, must-revalidate') - self.set_header('Pragma', 'no-cache') - self.set_header('Expires', '0') - - t = PageTemplate(headers=self.request.headers, file="home_newShow.tmpl") - t.submenu = HomeMenu() - - indexer, show_dir, indexer_id, show_name = self.split_extra_show(show_to_add) - - if indexer_id and indexer and show_name: - use_provided_info = True - else: - use_provided_info = False - - # tell the template whether we're giving it show name & Indexer ID - t.use_provided_info = use_provided_info - - # use the given show_dir for the indexer search if available - if use_show_name: - t.default_show_name = show_name - elif not show_dir: - t.default_show_name = '' - elif not show_name: - t.default_show_name = ek.ek(os.path.basename, ek.ek(os.path.normpath, show_dir)).replace('.', ' ') - else: - t.default_show_name = show_name - - # carry a list of other dirs if given - if not other_shows: - other_shows = [] - elif type(other_shows) != list: - other_shows = [other_shows] - - if use_provided_info: - t.provided_indexer_id = int(indexer_id or 0) - t.provided_indexer_name = show_name - - t.provided_show_dir = show_dir - t.other_shows = other_shows - t.provided_indexer = int(indexer or sickbeard.INDEXER_DEFAULT) - t.indexers = sickbeard.indexerApi().indexers - t.whitelist = [] - t.blacklist = [] - t.groups = [] - - return _munge(t) - - def recommendedShows(self, *args, **kwargs): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - self.set_header('Cache-Control', 'no-cache, no-store, must-revalidate') - self.set_header('Pragma', 'no-cache') - self.set_header('Expires', '0') - - t = PageTemplate(headers=self.request.headers, file="home_recommendedShows.tmpl") - t.submenu = HomeMenu() - - return _munge(t) - - def getRecommendedShows(self, *args, **kwargs): - final_results = [] - - logger.log(u"Getting recommended shows from Trakt.tv", logger.DEBUG) - recommendedlist = TraktCall("recommendations/shows.json/%API%", sickbeard.TRAKT_API, sickbeard.TRAKT_USERNAME, - sickbeard.TRAKT_PASSWORD) - - if recommendedlist == 'NULL': - logger.log(u"No shows found in your recommendedlist, aborting recommendedlist update", logger.DEBUG) - return - - if recommendedlist is None: - logger.log(u"Could not connect to trakt service, aborting recommended list update", logger.ERROR) - return - - map(final_results.append, - ([show['url'], - show['title'], - show['overview'], - sbdatetime.sbdatetime.sbfdate(datetime.date.fromtimestamp(int(show['first_aired']))), - sickbeard.indexerApi(1).name, - sickbeard.indexerApi(1).config['icon'], - int(show['tvdb_id'] or 0), - '%s%s' % (sickbeard.indexerApi(1).config['show_url'], int(show['tvdb_id'] or 0)), - sickbeard.indexerApi(2).name, - sickbeard.indexerApi(2).config['icon'], - int(show['tvrage_id'] or 0), - '%s%s' % (sickbeard.indexerApi(2).config['show_url'], int(show['tvrage_id'] or 0)) - ] for show in recommendedlist if not helpers.findCertainShow(sickbeard.showList, indexerid=int(show['tvdb_id'])))) - - self.set_header('Content-Type', 'application/json') - return json.dumps({'results': final_results}) - - def addRecommendedShow(self, whichSeries=None, indexerLang="en", rootDir=None, defaultStatus=None, - anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, - fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, - scene=None): - - indexer = 1 - indexer_name = sickbeard.indexerApi(int(indexer)).name - show_url = whichSeries.split('|')[1] - indexer_id = whichSeries.split('|')[0] - show_name = whichSeries.split('|')[2] - - return self.addNewShow('|'.join([indexer_name, str(indexer), show_url, indexer_id, show_name, ""]), - indexerLang, rootDir, - defaultStatus, - anyQualities, bestQualities, flatten_folders, subtitles, fullShowPath, other_shows, - skipShow, providedIndexer, anime, scene) - - def trendingShows(self, *args, **kwargs): - """ - Display the new show page which collects a tvdb id, folder, and extra options and - posts them to addNewShow - """ - t = PageTemplate(headers=self.request.headers, file="home_trendingShows.tmpl") - t.submenu = HomeMenu() - - t.trending_shows = TraktCall("shows/trending.json/%API%", sickbeard.TRAKT_API_KEY) - t.trending_inlibrary = 0 - if None is not t.trending_shows: - for item in t.trending_shows: - tvdbs = ['tvdb_id', 'tvrage_id'] - for index, tvdb in enumerate(tvdbs): - try: - item[u'show_id'] = str(item[tvdb]) - tvshow = helpers.findCertainShow(sickbeard.showList, int(item[tvdb])) - except: - continue - # check tvshow indexer is not using the same id from another indexer - if tvshow and (index + 1) == tvshow.indexer: - item[u'show_id'] = u'%s:%s' % (tvshow.indexer, item[tvdb]) - t.trending_inlibrary += 1 - break - - return _munge(t) - - def existingShows(self, *args, **kwargs): - """ - Prints out the page to add existing shows from a root dir - """ - t = PageTemplate(headers=self.request.headers, file="home_addExistingShow.tmpl") - t.submenu = HomeMenu() - t.whitelist = [] - t.blacklist = [] - t.groups = [] - - return _munge(t) - - def addTraktShow(self, indexer_id, showName): - if helpers.findCertainShow(sickbeard.showList, int(indexer_id)): - return - return self.newShow('|'.join(['', '', indexer_id, showName]), use_show_name=True) - - def addNewShow(self, whichSeries=None, indexerLang="en", rootDir=None, defaultStatus=None, - anyQualities=None, bestQualities=None, flatten_folders=None, subtitles=None, - fullShowPath=None, other_shows=None, skipShow=None, providedIndexer=None, anime=None, - scene=None, blacklist=None, whitelist=None): - """ - Receive tvdb id, dir, and other options and create a show from them. If extra show dirs are - provided then it forwards back to newShow, if not it goes to /home. - """ - - # grab our list of other dirs if given - if not other_shows: - other_shows = [] - elif type(other_shows) != list: - other_shows = [other_shows] - - def finishAddShow(): - # if there are no extra shows then go home - if not other_shows: - redirect('/home/') - - # peel off the next one - next_show_dir = other_shows[0] - rest_of_show_dirs = other_shows[1:] - - # go to add the next show - return self.newShow(next_show_dir, rest_of_show_dirs) - - # if we're skipping then behave accordingly - if skipShow: - return finishAddShow() - - # sanity check on our inputs - if (not rootDir and not fullShowPath) or not whichSeries: - return "Missing params, no Indexer ID or folder:" + repr(whichSeries) + " and " + repr( - rootDir) + "/" + repr(fullShowPath) - - # figure out what show we're adding and where - series_pieces = whichSeries.split('|') - if (whichSeries and rootDir) or (whichSeries and fullShowPath and len(series_pieces) > 1): - if len(series_pieces) < 6: - logger.log("Unable to add show due to show selection. Not anough arguments: %s" % (repr(series_pieces)), - logger.ERROR) - ui.notifications.error("Unknown error. Unable to add show due to problem with show selection.") - redirect('/home/addShows/existingShows/') - indexer = int(series_pieces[1]) - indexer_id = int(series_pieces[3]) - show_name = series_pieces[4] - else: - # if no indexer was provided use the default indexer set in General settings - if not providedIndexer: - providedIndexer = sickbeard.INDEXER_DEFAULT - - indexer = int(providedIndexer) - indexer_id = int(whichSeries) - show_name = os.path.basename(os.path.normpath(fullShowPath)) - - # use the whole path if it's given, or else append the show name to the root dir to get the full show path - if fullShowPath: - show_dir = ek.ek(os.path.normpath, fullShowPath) - else: - show_dir = ek.ek(os.path.join, rootDir, helpers.sanitizeFileName(show_name)) - - # blanket policy - if the dir exists you should have used "add existing show" numbnuts - if ek.ek(os.path.isdir, show_dir) and not fullShowPath: - ui.notifications.error("Unable to add show", "Folder " + show_dir + " exists already") - redirect('/home/addShows/existingShows/') - - # don't create show dir if config says not to - if sickbeard.ADD_SHOWS_WO_DIR: - logger.log(u"Skipping initial creation of " + show_dir + " due to config.ini setting") - else: - dir_exists = helpers.makeDir(show_dir) - if not dir_exists: - logger.log(u"Unable to create the folder " + show_dir + ", can't add the show", logger.ERROR) - ui.notifications.error("Unable to add show", - "Unable to create the folder " + show_dir + ", can't add the show") - redirect("/home/") - else: - helpers.chmodAsParent(show_dir) - - # prepare the inputs for passing along - scene = config.checkbox_to_value(scene) - anime = config.checkbox_to_value(anime) - flatten_folders = config.checkbox_to_value(flatten_folders) - subtitles = config.checkbox_to_value(subtitles) - - if whitelist: - whitelist = short_group_names(whitelist) - if blacklist: - blacklist = short_group_names(blacklist) - - if not anyQualities: - anyQualities = [] - if not bestQualities: - bestQualities = [] - if type(anyQualities) != list: - anyQualities = [anyQualities] - if type(bestQualities) != list: - bestQualities = [bestQualities] - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - - # add the show - sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, int(defaultStatus), newQuality, - flatten_folders, indexerLang, subtitles, anime, - scene, None, blacklist, whitelist) # @UndefinedVariable - ui.notifications.message('Show added', 'Adding the specified show into ' + show_dir) - - return finishAddShow() - - def split_extra_show(self, extra_show): - if not extra_show: - return (None, None, None, None) - split_vals = extra_show.split('|') - if len(split_vals) < 4: - indexer = split_vals[0] - show_dir = split_vals[1] - return (indexer, show_dir, None, None) - indexer = split_vals[0] - show_dir = split_vals[1] - indexer_id = split_vals[2] - show_name = '|'.join(split_vals[3:]) - - return (indexer, show_dir, indexer_id, show_name) - - def addExistingShows(self, shows_to_add=None, promptForSettings=None): - """ - Receives a dir list and add them. Adds the ones with given TVDB IDs first, then forwards - along to the newShow page. - """ - - # grab a list of other shows to add, if provided - if not shows_to_add: - shows_to_add = [] - elif type(shows_to_add) != list: - shows_to_add = [shows_to_add] - - shows_to_add = [urllib.unquote_plus(x) for x in shows_to_add] - - promptForSettings = config.checkbox_to_value(promptForSettings) - - indexer_id_given = [] - dirs_only = [] - # separate all the ones with Indexer IDs - for cur_dir in shows_to_add: - if '|' in cur_dir: - split_vals = cur_dir.split('|') - if len(split_vals) < 3: - dirs_only.append(cur_dir) - if not '|' in cur_dir: - dirs_only.append(cur_dir) - else: - indexer, show_dir, indexer_id, show_name = self.split_extra_show(cur_dir) - - if not show_dir or not indexer_id or not show_name: - continue - - indexer_id_given.append((int(indexer), show_dir, int(indexer_id), show_name)) - - - # if they want me to prompt for settings then I will just carry on to the newShow page - if promptForSettings and shows_to_add: - return self.newShow(shows_to_add[0], shows_to_add[1:]) - - # if they don't want me to prompt for settings then I can just add all the nfo shows now - num_added = 0 - for cur_show in indexer_id_given: - indexer, show_dir, indexer_id, show_name = cur_show - - if indexer is not None and indexer_id is not None: - # add the show - sickbeard.showQueueScheduler.action.addShow(indexer, indexer_id, show_dir, - default_status=sickbeard.STATUS_DEFAULT, - quality=sickbeard.QUALITY_DEFAULT, - flatten_folders=sickbeard.FLATTEN_FOLDERS_DEFAULT, - subtitles=sickbeard.SUBTITLES_DEFAULT, - anime=sickbeard.ANIME_DEFAULT, - scene=sickbeard.SCENE_DEFAULT) - num_added += 1 - - if num_added: - ui.notifications.message("Shows Added", - "Automatically added " + str(num_added) + " from their existing metadata files") - - # if we're done then go home - if not dirs_only: - redirect('/home/') - - # for the remaining shows we need to prompt for each one, so forward this on to the newShow page - return self.newShow(dirs_only[0], dirs_only[1:]) - - -ErrorLogsMenu = [ - {'title': 'Clear Errors', 'path': 'errorlogs/clearerrors/'}, - # { 'title': 'View Log', 'path': 'errorlogs/viewlog' }, -] + return json.dumps(messages) class ErrorLogs(MainHandler): + @staticmethod + def ErrorLogsMenu(): + return [{'title': 'Clear Errors', 'path': 'errorlogs/clearerrors/'},] + def index(self, *args, **kwargs): - t = PageTemplate(headers=self.request.headers, file="errorlogs.tmpl") - t.submenu = ErrorLogsMenu + t = PageTemplate(headers=self.request.headers, file='errorlogs.tmpl') + t.submenu = self.ErrorLogsMenu - return _munge(t) + return t.respond() def clearerrors(self, *args, **kwargs): classes.ErrorViewer.clear() - redirect("/errorlogs/") + self.redirect('/errorlogs/') def viewlog(self, minLevel=logger.MESSAGE, maxLines=500): - t = PageTemplate(headers=self.request.headers, file="viewlogs.tmpl") - t.submenu = ErrorLogsMenu + t = PageTemplate(headers=self.request.headers, file='viewlogs.tmpl') + t.submenu = self.ErrorLogsMenu minLevel = int(minLevel) @@ -3172,7 +4362,7 @@ class ErrorLogs(MainHandler): with ek.ek(open, logger.sb_log_instance.log_file_path) as f: data = f.readlines() - regex = "^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$" + regex = '^(\d\d\d\d)\-(\d\d)\-(\d\d)\s*(\d\d)\:(\d\d):(\d\d)\s*([A-Z]+)\s*(.+?)\s*\:\:\s*(.*)$' finalData = [] @@ -3199,1286 +4389,61 @@ class ErrorLogs(MainHandler): continue elif lastLine: - finalData.append("AA" + x) + finalData.append('AA' + x) numLines += 1 if numLines >= numToShow: break - result = "".join(finalData) + result = ''.join(finalData) t.logLines = result t.minLevel = minLevel - return _munge(t) + return t.respond() -class Home(MainHandler): - def index(self, *args, **kwargs): +class WebFileBrowser(MainHandler): + def index(self, path='', includeFiles=False, *args, **kwargs): + self.set_header('Content-Type', 'application/json') + return json.dumps(foldersAtPath(path, True, bool(int(includeFiles)))) - t = PageTemplate(headers=self.request.headers, file="home.tmpl") - if sickbeard.ANIME_SPLIT_HOME: - shows = [] - anime = [] - for show in sickbeard.showList: - if show.is_anime: - anime.append(show) - else: - shows.append(show) - t.showlists = [["Shows", shows], - ["Anime", anime]] - else: - t.showlists = [["Shows", sickbeard.showList]] + def complete(self, term, includeFiles=0): + self.set_header('Content-Type', 'application/json') + paths = [entry['path'] for entry in foldersAtPath(os.path.dirname(term), includeFiles=bool(int(includeFiles))) if 'path' in entry] + return json.dumps(paths) - t.submenu = HomeMenu() - return _munge(t) - - addShows = NewHomeAddShows - postprocess = HomePostProcess - - def testSABnzbd(self, host=None, username=None, password=None, apikey=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_url(host) - - connection, accesMsg = sab.getSabAccesMethod(host, username, password, apikey) - if connection: - authed, authMsg = sab.testAuthentication(host, username, password, apikey) # @UnusedVariable - if authed: - return "Success. Connected and authenticated" - else: - return "Authentication failed. SABnzbd expects '" + accesMsg + "' as authentication method" - else: - return "Unable to connect to host" - - def testTorrent(self, torrent_method=None, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_url(host) - - client = clients.getClientIstance(torrent_method) - - connection, accesMsg = client(host, username, password).testAuthentication() - - return accesMsg - - def testGrowl(self, host=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host, default_port=23053) - - result = notifiers.growl_notifier.test_notify(host, password) - if password is None or password == '': - pw_append = '' - else: - pw_append = " with password: " + password - - if result: - return "Registered and Tested growl successfully " + urllib.unquote_plus(host) + pw_append - else: - return "Registration and Testing of growl failed " + urllib.unquote_plus(host) + pw_append - - def testProwl(self, prowl_api=None, prowl_priority=0): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.prowl_notifier.test_notify(prowl_api, prowl_priority) - if result: - return "Test prowl notice sent successfully" - else: - return "Test prowl notice failed" - - def testBoxcar2(self, accesstoken=None, sound=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.boxcar2_notifier.test_notify(accesstoken, sound) - if result: - return "Boxcar2 notification succeeded. Check your Boxcar2 clients to make sure it worked" - else: - return "Error sending Boxcar2 notification" - - def testPushover(self, userKey=None, apiKey=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.pushover_notifier.test_notify(userKey, apiKey) - if result: - return "Pushover notification succeeded. Check your Pushover clients to make sure it worked" - else: - return "Error sending Pushover notification" - - def twitterStep1(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - return notifiers.twitter_notifier._get_authorization() - - def twitterStep2(self, key): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.twitter_notifier._get_credentials(key) - logger.log(u"result: " + str(result)) - if result: - return "Key verification successful" - else: - return "Unable to verify key" - - def testTwitter(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.twitter_notifier.test_notify() - if result: - return "Tweet successful, check your twitter to make sure it worked" - else: - return "Error sending tweet" - - def testXBMC(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_hosts(host) - finalResult = '' - for curHost in [x.strip() for x in host.split(",")]: - curResult = notifiers.xbmc_notifier.test_notify(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(":")) > 2 and 'OK' in curResult.split(":")[2]: - finalResult += "Test XBMC notice sent successfully to " + urllib.unquote_plus(curHost) - else: - finalResult += "Test XBMC notice failed to " + urllib.unquote_plus(curHost) - finalResult += "
    \n" - - return finalResult - - def testPMC(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - finalResult = '' - for curHost in [x.strip() for x in host.split(',')]: - curResult = notifiers.plex_notifier.test_notify_pmc(urllib.unquote_plus(curHost), username, password) - if len(curResult.split(':')) > 2 and 'OK' in curResult.split(':')[2]: - finalResult += 'Successful test notice sent to Plex client ... ' + urllib.unquote_plus(curHost) - else: - finalResult += 'Test failed for Plex client ... ' + urllib.unquote_plus(curHost) - finalResult += '
    ' + "\n" - - ui.notifications.message('Tested Plex client(s): ', urllib.unquote_plus(host.replace(',', ', '))) - - return finalResult - - def testPMS(self, host=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - finalResult = '' - - curResult = notifiers.plex_notifier.test_notify_pms(urllib.unquote_plus(host), username, password) - if None is curResult: - finalResult += 'Successful test of Plex server(s) ... ' + urllib.unquote_plus(host.replace(',', ', ')) - else: - finalResult += 'Test failed for Plex server(s) ... ' + urllib.unquote_plus(curResult.replace(',', ', ')) - finalResult += '
    ' + "\n" - - ui.notifications.message('Tested Plex Media Server host(s): ', urllib.unquote_plus(host.replace(',', ', '))) - - return finalResult - - def testLibnotify(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - if notifiers.libnotify_notifier.test_notify(): - return "Tried sending desktop notification via libnotify" - else: - return notifiers.libnotify.diagnose() - - def testNMJ(self, host=None, database=None, mount=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmj_notifier.test_notify(urllib.unquote_plus(host), database, mount) - if result: - return "Successfully started the scan update" - else: - return "Test failed to start the scan update" - - def settingsNMJ(self, host=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmj_notifier.notify_settings(urllib.unquote_plus(host)) - if result: - return '{"message": "Got settings from %(host)s", "database": "%(database)s", "mount": "%(mount)s"}' % { - "host": host, "database": sickbeard.NMJ_DATABASE, "mount": sickbeard.NMJ_MOUNT} - else: - return '{"message": "Failed! Make sure your Popcorn is on and NMJ is running. (see Log & Errors -> Debug for detailed info)", "database": "", "mount": ""}' - - def testNMJv2(self, host=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmjv2_notifier.test_notify(urllib.unquote_plus(host)) - if result: - return "Test notice sent successfully to " + urllib.unquote_plus(host) - else: - return "Test notice failed to " + urllib.unquote_plus(host) - - def settingsNMJv2(self, host=None, dbloc=None, instance=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - result = notifiers.nmjv2_notifier.notify_settings(urllib.unquote_plus(host), dbloc, instance) - if result: - return '{"message": "NMJ Database found at: %(host)s", "database": "%(database)s"}' % {"host": host, - "database": sickbeard.NMJv2_DATABASE} - else: - return '{"message": "Unable to find NMJ Database at location: %(dbloc)s. Is the right location selected and PCH running?", "database": ""}' % { - "dbloc": dbloc} - - def testTrakt(self, api=None, username=None, password=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.trakt_notifier.test_notify(api, username, password) - if result: - return "Test notice sent successfully to Trakt" - else: - return "Test notice failed to Trakt" - - def loadShowNotifyLists(self, *args, **kwargs): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - myDB = db.DBConnection() - rows = myDB.select("SELECT show_id, show_name, notify_list FROM tv_shows ORDER BY show_name ASC") - - data = {} - size = 0 - for r in rows: - data[r['show_id']] = {'id': r['show_id'], 'name': r['show_name'], 'list': r['notify_list']} - size += 1 - data['_size'] = size - return json.dumps(data) - - def testEmail(self, host=None, port=None, smtp_from=None, use_tls=None, user=None, pwd=None, to=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - host = config.clean_host(host) - if notifiers.email_notifier.test_notify(host, port, smtp_from, use_tls, user, pwd, to): - return 'Test email sent successfully! Check inbox.' - else: - return 'ERROR: %s' % notifiers.email_notifier.last_err - - def testNMA(self, nma_api=None, nma_priority=0): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.nma_notifier.test_notify(nma_api, nma_priority) - if result: - return "Test NMA notice sent successfully" - else: - return "Test NMA notice failed" - - def testPushalot(self, authorizationToken=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.pushalot_notifier.test_notify(authorizationToken) - if result: - return "Pushalot notification succeeded. Check your Pushalot clients to make sure it worked" - else: - return "Error sending Pushalot notification" - - def testPushbullet(self, api=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.pushbullet_notifier.test_notify(api) - if result: - return "Pushbullet notification succeeded. Check your device to make sure it worked" - else: - return "Error sending Pushbullet notification" - - def getPushbulletDevices(self, api=None): - self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') - - result = notifiers.pushbullet_notifier.get_devices(api) - if result: - return result - else: - return "Error sending Pushbullet notification" - - def shutdown(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home/") - - sickbeard.events.put(sickbeard.events.SystemEvent.SHUTDOWN) - - title = "Shutting down" - message = "SickGear is shutting down..." - - return self._genericMessage(title, message) - - def restart(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home/") - - t = PageTemplate(headers=self.request.headers, file="restart.tmpl") - t.submenu = HomeMenu() - - # restart - sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) - - return _munge(t) - - def update(self, pid=None): - - if str(pid) != str(sickbeard.PID): - redirect("/home/") - - updated = sickbeard.versionCheckScheduler.action.update() # @UndefinedVariable - if updated: - # do a hard restart - sickbeard.events.put(sickbeard.events.SystemEvent.RESTART) - - t = PageTemplate(headers=self.request.headers, file="restart_bare.tmpl") - return _munge(t) - else: - return self._genericMessage("Update Failed", - "Update wasn't successful, not restarting. Check your log for more information.") - - def branchCheckout(self, branch): - sickbeard.BRANCH = branch - ui.notifications.message('Checking out branch: ', branch) - return self.update(sickbeard.PID) - - def pullRequestCheckout(self, branch): - pull_request = branch - branch = branch.split(':')[1] - fetched = sickbeard.versionCheckScheduler.action.fetch(pull_request) - if fetched: - sickbeard.BRANCH = branch - ui.notifications.message('Checking out branch: ', branch) - return self.update(sickbeard.PID) - else: - return redirect('/home/') - - def displayShow(self, show=None): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - else: - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Show not in show list") - - myDB = db.DBConnection() - seasonResults = myDB.select( - "SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season desc", - [showObj.indexerid] - ) - - sqlResults = myDB.select( - "SELECT * FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC", - [showObj.indexerid] - ) - - t = PageTemplate(headers=self.request.headers, file="displayShow.tmpl") - t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}] - - try: - t.showLoc = (showObj.location, True) - except sickbeard.exceptions.ShowDirNotFoundException: - t.showLoc = (showObj._location, False) - - show_message = '' - - if sickbeard.showQueueScheduler.action.isBeingAdded(showObj): # @UndefinedVariable - show_message = 'This show is in the process of being downloaded - the info below is incomplete.' - - elif sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable - show_message = 'The information on this page is in the process of being updated.' - - elif sickbeard.showQueueScheduler.action.isBeingRefreshed(showObj): # @UndefinedVariable - show_message = 'The episodes below are currently being refreshed from disk' - - elif sickbeard.showQueueScheduler.action.isBeingSubtitled(showObj): # @UndefinedVariable - show_message = 'Currently downloading subtitles for this show' - - elif sickbeard.showQueueScheduler.action.isInRefreshQueue(showObj): # @UndefinedVariable - show_message = 'This show is queued to be refreshed.' - - elif sickbeard.showQueueScheduler.action.isInUpdateQueue(showObj): # @UndefinedVariable - show_message = 'This show is queued and awaiting an update.' - - elif sickbeard.showQueueScheduler.action.isInSubtitleQueue(showObj): # @UndefinedVariable - show_message = 'This show is queued and awaiting subtitles download.' - - if not sickbeard.showQueueScheduler.action.isBeingAdded(showObj): # @UndefinedVariable - if not sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable - t.submenu.append( - {'title': 'Remove', 'path': 'home/deleteShow?show=%d' % showObj.indexerid, 'confirm': True}) - t.submenu.append({'title': 'Re-scan files', 'path': 'home/refreshShow?show=%d' % showObj.indexerid}) - t.submenu.append( - {'title': 'Force Full Update', 'path': 'home/updateShow?show=%d&force=1' % showObj.indexerid}) - t.submenu.append({'title': 'Update show in XBMC', - 'path': 'home/updateXBMC?showName=%s' % urllib.quote_plus( - showObj.name.encode('utf-8')), 'requires': haveXBMC}) - t.submenu.append({'title': 'Preview Rename', 'path': 'home/testRename?show=%d' % showObj.indexerid}) - if sickbeard.USE_SUBTITLES and not sickbeard.showQueueScheduler.action.isBeingSubtitled( - showObj) and showObj.subtitles: - t.submenu.append( - {'title': 'Download Subtitles', 'path': 'home/subtitleShow?show=%d' % showObj.indexerid}) - - t.show = showObj - t.sqlResults = sqlResults - t.seasonResults = seasonResults - t.show_message = show_message - - epCounts = {} - epCats = {} - epCounts[Overview.SKIPPED] = 0 - epCounts[Overview.WANTED] = 0 - epCounts[Overview.QUAL] = 0 - epCounts[Overview.GOOD] = 0 - epCounts[Overview.UNAIRED] = 0 - epCounts[Overview.SNATCHED] = 0 - - for curResult in sqlResults: - curEpCat = showObj.getOverview(int(curResult["status"])) - if curEpCat: - epCats[str(curResult["season"]) + "x" + str(curResult["episode"])] = curEpCat - epCounts[curEpCat] += 1 +class ApiBuilder(MainHandler): + def index(self): + """ expose the api-builder template """ + t = PageTemplate(headers=self.request.headers, file='apiBuilder.tmpl') def titler(x): return (remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] - if sickbeard.ANIME_SPLIT_HOME: - shows = [] - anime = [] - for show in sickbeard.showList: - if show.is_anime: - anime.append(show) - else: - shows.append(show) - t.sortedShowLists = [["Shows", sorted(shows, lambda x, y: cmp(titler(x.name), titler(y.name)))], - ["Anime", sorted(anime, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + t.sortedShowList = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) + seasonSQLResults = {} + episodeSQLResults = {} + + myDB = db.DBConnection(row_type='dict') + for curShow in t.sortedShowList: + seasonSQLResults[curShow.indexerid] = myDB.select( + 'SELECT DISTINCT season FROM tv_episodes WHERE showid = ? ORDER BY season DESC', [curShow.indexerid]) + + for curShow in t.sortedShowList: + episodeSQLResults[curShow.indexerid] = myDB.select( + 'SELECT DISTINCT season,episode FROM tv_episodes WHERE showid = ? ORDER BY season DESC, episode DESC', + [curShow.indexerid]) + + t.seasonSQLResults = seasonSQLResults + t.episodeSQLResults = episodeSQLResults + + if len(sickbeard.API_KEY) == 32: + t.apikey = sickbeard.API_KEY else: - t.sortedShowLists = [ - ["Shows", sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name)))]] + t.apikey = 'api key not generated' - tvshows = [] - for tvshow_types in t.sortedShowLists: - for tvshow in tvshow_types[1]: - tvshows.append(tvshow.indexerid) - t.tvshow_id_csv = ','.join(str(x) for x in tvshows) - - t.bwl = None - if showObj.is_anime: - t.bwl = showObj.release_groups - - t.epCounts = epCounts - t.epCats = epCats - - showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.indexerid) - - indexerid = int(showObj.indexerid) - indexer = int(showObj.indexer) - t.all_scene_exceptions = showObj.exceptions - t.scene_numbering = get_scene_numbering_for_show(indexerid, indexer) - t.xem_numbering = get_xem_numbering_for_show(indexerid, indexer) - t.scene_absolute_numbering = get_scene_absolute_numbering_for_show(indexerid, indexer) - t.xem_absolute_numbering = get_xem_absolute_numbering_for_show(indexerid, indexer) - - return _munge(t) - - def plotDetails(self, show, season, episode): - myDB = db.DBConnection() - result = myDB.select( - "SELECT description FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ?", - (int(show), int(season), int(episode))) - return result[0]['description'] if result else 'Episode not found.' - - def sceneExceptions(self, show): - exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(show) - if not exceptionsList: - return "No scene exceptions" - - out = [] - for season, names in iter(sorted(exceptionsList.iteritems())): - if season == -1: - season = "*" - out.append("S" + str(season) + ": " + ", ".join(names)) - return "
    ".join(out) - - def editShow(self, show=None, location=None, anyQualities=[], bestQualities=[], exceptions_list=[], - flatten_folders=None, paused=None, directCall=False, air_by_date=None, sports=None, dvdorder=None, - indexerLang=None, subtitles=None, archive_firstmatch=None, rls_ignore_words=None, - rls_require_words=None, anime=None, blacklist=None, whitelist=None, - scene=None): - - if show is None: - errString = "Invalid show ID: " + str(show) - if directCall: - return [errString] - else: - return self._genericMessage("Error", errString) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if not showObj: - errString = "Unable to find the specified show: " + str(show) - if directCall: - return [errString] - else: - return self._genericMessage("Error", errString) - - showObj.exceptions = scene_exceptions.get_scene_exceptions(showObj.indexerid) - - if not location and not anyQualities and not bestQualities and not flatten_folders: - t = PageTemplate(headers=self.request.headers, file="editShow.tmpl") - t.submenu = HomeMenu() - - if showObj.is_anime: - t.whitelist = showObj.release_groups.whitelist - t.blacklist = showObj.release_groups.blacklist - - t.groups = [] - if helpers.set_up_anidb_connection(): - try: - anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=showObj.name) - t.groups = anime.get_groups() - except Exception, e: - t.groups.append(dict([('name', 'Fail:AniDB connect. Restart sg else check debug log'), ('rating', ''), ('range', '')])) - else: - t.groups.append(dict([('name', 'Did not initialise AniDB. Check debug log if reqd.'), ('rating', ''), ('range', '')])) - - with showObj.lock: - t.show = showObj - t.scene_exceptions = get_scene_exceptions(showObj.indexerid) - - return _munge(t) - - flatten_folders = config.checkbox_to_value(flatten_folders) - dvdorder = config.checkbox_to_value(dvdorder) - archive_firstmatch = config.checkbox_to_value(archive_firstmatch) - paused = config.checkbox_to_value(paused) - air_by_date = config.checkbox_to_value(air_by_date) - scene = config.checkbox_to_value(scene) - sports = config.checkbox_to_value(sports) - anime = config.checkbox_to_value(anime) - subtitles = config.checkbox_to_value(subtitles) - - if indexerLang and indexerLang in sickbeard.indexerApi(showObj.indexer).indexer().config['valid_languages']: - indexer_lang = indexerLang - else: - indexer_lang = showObj.lang - - # if we changed the language then kick off an update - if indexer_lang == showObj.lang: - do_update = False - else: - do_update = True - - if scene == showObj.scene and anime == showObj.anime: - do_update_scene_numbering = False - else: - do_update_scene_numbering = True - - if type(anyQualities) != list: - anyQualities = [anyQualities] - - if type(bestQualities) != list: - bestQualities = [bestQualities] - - if type(exceptions_list) != list: - exceptions_list = [exceptions_list] - - # If directCall from mass_edit_update no scene exceptions handling or blackandwhite list handling - if directCall: - do_update_exceptions = False - else: - if set(exceptions_list) == set(showObj.exceptions): - do_update_exceptions = False - else: - do_update_exceptions = True - - with showObj.lock: - if anime: - if not showObj.release_groups: - showObj.release_groups = BlackAndWhiteList(showObj.indexerid) - if whitelist: - shortwhitelist = short_group_names(whitelist) - showObj.release_groups.set_white_keywords(shortwhitelist) - else: - showObj.release_groups.set_white_keywords([]) - - if blacklist: - shortblacklist = short_group_names(blacklist) - showObj.release_groups.set_black_keywords(shortblacklist) - else: - showObj.release_groups.set_black_keywords([]) - - errors = [] - with showObj.lock: - newQuality = Quality.combineQualities(map(int, anyQualities), map(int, bestQualities)) - showObj.quality = newQuality - showObj.archive_firstmatch = archive_firstmatch - - # reversed for now - if bool(showObj.flatten_folders) != bool(flatten_folders): - showObj.flatten_folders = flatten_folders - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show: " + ex(e)) - - showObj.paused = paused - showObj.scene = scene - showObj.anime = anime - showObj.sports = sports - showObj.subtitles = subtitles - showObj.air_by_date = air_by_date - - if not directCall: - showObj.lang = indexer_lang - showObj.dvdorder = dvdorder - showObj.rls_ignore_words = rls_ignore_words.strip() - showObj.rls_require_words = rls_require_words.strip() - - # if we change location clear the db of episodes, change it, write to db, and rescan - if os.path.normpath(showObj._location) != os.path.normpath(location): - logger.log(os.path.normpath(showObj._location) + " != " + os.path.normpath(location), logger.DEBUG) - if not ek.ek(os.path.isdir, location) and not sickbeard.CREATE_MISSING_SHOW_DIRS: - errors.append("New location %s does not exist" % location) - - # don't bother if we're going to update anyway - elif not do_update: - # change it - try: - showObj.location = location - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable - except exceptions.CantRefreshException, e: - errors.append("Unable to refresh this show:" + ex(e)) - # grab updated info from TVDB - # showObj.loadEpisodesFromIndexer() - # rescan the episodes in the new folder - except exceptions.NoNFOException: - errors.append( - "The folder at %s doesn't contain a tvshow.nfo - copy your files to that folder before you change the directory in SickGear." % location) - - # save it to the DB - showObj.saveToDB() - - # force the update - if do_update: - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, True) # @UndefinedVariable - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - except exceptions.CantUpdateException, e: - errors.append("Unable to force an update on the show.") - - if do_update_exceptions: - try: - scene_exceptions.update_scene_exceptions(showObj.indexerid, exceptions_list) # @UndefinedVdexerid) - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - except exceptions.CantUpdateException, e: - errors.append("Unable to force an update on scene exceptions of the show.") - - if do_update_scene_numbering: - try: - sickbeard.scene_numbering.xem_refresh(showObj.indexerid, showObj.indexer) # @UndefinedVariable - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - except exceptions.CantUpdateException, e: - errors.append("Unable to force an update on scene numbering of the show.") - - if directCall: - return errors - - if len(errors) > 0: - ui.notifications.error('%d error%s while saving changes:' % (len(errors), "" if len(errors) == 1 else "s"), - '
      ' + '\n'.join(['
    • %s
    • ' % error for error in errors]) + "
    ") - - redirect("/home/displayShow?show=" + show) - - def deleteShow(self, show=None, full=0): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Unable to find the specified show") - - if sickbeard.showQueueScheduler.action.isBeingAdded( - showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): # @UndefinedVariable - return self._genericMessage("Error", "Shows can't be deleted while they're being added or updated.") - - if sickbeard.USE_TRAKT and sickbeard.TRAKT_SYNC: - # remove show from trakt.tv library - sickbeard.traktCheckerScheduler.action.removeShowFromTraktLibrary(showObj) - - showObj.deleteShow(bool(full)) - - ui.notifications.message('%s with %s' % (('Deleting', 'Trashing')[sickbeard.TRASH_REMOVE_SHOW], - ('media left untouched', 'all related media')[bool(full)]), - '%s' % showObj.name) - redirect("/home/") - - def refreshShow(self, show=None): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Unable to find the specified show") - - # force the update from the DB - try: - sickbeard.showQueueScheduler.action.refreshShow(showObj) # @UndefinedVariable - except exceptions.CantRefreshException, e: - ui.notifications.error("Unable to refresh this show.", - ex(e)) - - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - redirect("/home/displayShow?show=" + str(showObj.indexerid)) - - def updateShow(self, show=None, force=0): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Unable to find the specified show") - - # force the update - try: - sickbeard.showQueueScheduler.action.updateShow(showObj, bool(force)) # @UndefinedVariable - except exceptions.CantUpdateException, e: - ui.notifications.error("Unable to update this show.", - ex(e)) - - # just give it some time - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - redirect("/home/displayShow?show=" + str(showObj.indexerid)) - - def subtitleShow(self, show=None, force=0): - - if show is None: - return self._genericMessage("Error", "Invalid show ID") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Unable to find the specified show") - - # search and download subtitles - sickbeard.showQueueScheduler.action.downloadSubtitles(showObj, bool(force)) # @UndefinedVariable - - time.sleep(cpu_presets[sickbeard.CPU_PRESET]) - - redirect("/home/displayShow?show=" + str(showObj.indexerid)) - - def updateXBMC(self, showName=None): - - # only send update to first host in the list -- workaround for xbmc sql backend users - if sickbeard.XBMC_UPDATE_ONLYFIRST: - # only send update to first host in the list -- workaround for xbmc sql backend users - host = sickbeard.XBMC_HOST.split(",")[0].strip() - else: - host = sickbeard.XBMC_HOST - - if notifiers.xbmc_notifier.update_library(showName=showName): - ui.notifications.message("Library update command sent to XBMC host(s): " + host) - else: - ui.notifications.error("Unable to contact one or more XBMC host(s): " + host) - redirect('/home/') - - def updatePLEX(self, *args, **kwargs): - result = notifiers.plex_notifier.update_library() - if None is result: - ui.notifications.message( - 'Library update command sent to', 'Plex Media Server host(s): ' + sickbeard.PLEX_SERVER_HOST.replace(',', ', ')) - else: - ui.notifications.error('Unable to contact', 'Plex Media Server host(s): ' + result.replace(',', ', ')) - redirect('/home/') - - def setStatus(self, show=None, eps=None, status=None, direct=False): - - if show is None or eps is None or status is None: - errMsg = "You must specify a show and at least one episode" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return self._genericMessage("Error", errMsg) - - if not statusStrings.has_key(int(status)): - errMsg = "Invalid status" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return self._genericMessage("Error", errMsg) - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - errMsg = "Error", "Show not in show list" - if direct: - ui.notifications.error('Error', errMsg) - return json.dumps({'result': 'error'}) - else: - return self._genericMessage("Error", errMsg) - - segments = {} - if eps is not None: - - sql_l = [] - for curEp in eps.split('|'): - - logger.log(u"Attempting to set status on episode " + curEp + " to " + status, logger.DEBUG) - - epInfo = curEp.split('x') - - epObj = showObj.getEpisode(int(epInfo[0]), int(epInfo[1])) - - if epObj is None: - return self._genericMessage("Error", "Episode couldn't be retrieved") - - if int(status) in [WANTED, FAILED]: - # figure out what episodes are wanted so we can backlog them - if epObj.season in segments: - segments[epObj.season].append(epObj) - else: - segments[epObj.season] = [epObj] - - with epObj.lock: - # don't let them mess up UNAIRED episodes - if epObj.status == UNAIRED: - logger.log(u"Refusing to change status of " + curEp + " because it is UNAIRED", logger.ERROR) - continue - - if int( - status) in Quality.DOWNLOADED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED + [ - IGNORED] and not ek.ek(os.path.isfile, epObj.location): - logger.log( - u"Refusing to change status of " + curEp + " to DOWNLOADED because it's not SNATCHED/DOWNLOADED", - logger.ERROR) - continue - - if int( - status) == FAILED and epObj.status not in Quality.SNATCHED + Quality.SNATCHED_PROPER + Quality.DOWNLOADED: - logger.log( - u"Refusing to change status of " + curEp + " to FAILED because it's not SNATCHED/DOWNLOADED", - logger.ERROR) - continue - - epObj.status = int(status) - - # mass add to database - result = epObj.get_sql() - if None is not result: - sql_l.append(result) - - if len(sql_l) > 0: - myDB = db.DBConnection() - myDB.mass_action(sql_l) - - if int(status) == WANTED: - msg = "Backlog was automatically started for the following seasons of " + showObj.name + ":
    " - msg += '
      ' - - for season, segment in segments.items(): - cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment) - sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) # @UndefinedVariable - - msg += "
    • Season " + str(season) + "
    • " - logger.log(u"Sending backlog for " + showObj.name + " season " + str( - season) + " because some eps were set to wanted") - - msg += "
    " - - if segments: - ui.notifications.message("Backlog started", msg) - - if int(status) == FAILED: - msg = "Retrying Search was automatically started for the following season of " + showObj.name + ":
    " - msg += '
      ' - - for season, segment in segments.items(): - cur_failed_queue_item = search_queue.FailedQueueItem(showObj, [segment]) - sickbeard.searchQueueScheduler.action.add_item(cur_failed_queue_item) # @UndefinedVariable - - msg += "
    • Season " + str(season) + "
    • " - logger.log(u"Retrying Search for " + showObj.name + " season " + str( - season) + " because some eps were set to failed") - - msg += "
    " - - if segments: - ui.notifications.message("Retry Search started", msg) - - if direct: - return json.dumps({'result': 'success'}) - else: - redirect("/home/displayShow?show=" + show) - - def testRename(self, show=None): - - if show is None: - return self._genericMessage("Error", "You must specify a show") - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj is None: - return self._genericMessage("Error", "Show not in show list") - - try: - show_loc = showObj.location # @UnusedVariable - except exceptions.ShowDirNotFoundException: - return self._genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - ep_obj_rename_list = [] - - ep_obj_list = showObj.getAllEpisodes(has_location=True) - - for cur_ep_obj in ep_obj_list: - # Only want to rename if we have a location - if cur_ep_obj.location: - if cur_ep_obj.relatedEps: - # do we have one of multi-episodes in the rename list already - have_already = False - for cur_related_ep in cur_ep_obj.relatedEps + [cur_ep_obj]: - if cur_related_ep in ep_obj_rename_list: - have_already = True - break - if not have_already: - ep_obj_rename_list.append(cur_ep_obj) - else: - ep_obj_rename_list.append(cur_ep_obj) - - if ep_obj_rename_list: - # present season DESC episode DESC on screen - ep_obj_rename_list.reverse() - - t = PageTemplate(headers=self.request.headers, file="testRename.tmpl") - t.submenu = [{'title': 'Edit', 'path': 'home/editShow?show=%d' % showObj.indexerid}] - t.ep_obj_list = ep_obj_rename_list - t.show = showObj - - return _munge(t) - - def doRename(self, show=None, eps=None): - - if show is None or eps is None: - errMsg = "You must specify a show and at least one episode" - return self._genericMessage("Error", errMsg) - - show_obj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if show_obj is None: - errMsg = "Error", "Show not in show list" - return self._genericMessage("Error", errMsg) - - try: - show_loc = show_obj.location # @UnusedVariable - except exceptions.ShowDirNotFoundException: - return self._genericMessage("Error", "Can't rename episodes when the show dir is missing.") - - if eps is None: - redirect("/home/displayShow?show=" + show) - - myDB = db.DBConnection() - for curEp in eps.split('|'): - - epInfo = curEp.split('x') - - # this is probably the worst possible way to deal with double eps but I've kinda painted myself into a corner here with this stupid database - ep_result = myDB.select( - "SELECT * FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND 5=5", - [show, epInfo[0], epInfo[1]]) - if not ep_result: - logger.log(u"Unable to find an episode for " + curEp + ", skipping", logger.WARNING) - continue - related_eps_result = myDB.select("SELECT * FROM tv_episodes WHERE location = ? AND episode != ?", - [ep_result[0]["location"], epInfo[1]]) - - root_ep_obj = show_obj.getEpisode(int(epInfo[0]), int(epInfo[1])) - root_ep_obj.relatedEps = [] - - for cur_related_ep in related_eps_result: - related_ep_obj = show_obj.getEpisode(int(cur_related_ep["season"]), int(cur_related_ep["episode"])) - if related_ep_obj not in root_ep_obj.relatedEps: - root_ep_obj.relatedEps.append(related_ep_obj) - - root_ep_obj.rename() - - redirect("/home/displayShow?show=" + show) - - def searchEpisode(self, show=None, season=None, episode=None): - - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.ManualSearchQueueItem(ep_obj.show, ep_obj) - - sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable - - if ep_queue_item.success: - return returnManualSearchResult(ep_queue_item) - if not ep_queue_item.started and ep_queue_item.success is None: - return json.dumps({'result': 'success'}) #I Actually want to call it queued, because the search hasnt been started yet! - if ep_queue_item.started and ep_queue_item.success is None: - return json.dumps({'result': 'success'}) - else: - return json.dumps({'result': 'failure'}) - - ### Returns the current ep_queue_item status for the current viewed show. - # Possible status: Downloaded, Snatched, etc... - # Returns {'show': 279530, 'episodes' : ['episode' : 6, 'season' : 1, 'searchstatus' : 'queued', 'status' : 'running', 'quality': '4013'] - def getManualSearchStatus(self, show=None, season=None): - - episodes = [] - currentManualSearchThreadsQueued = [] - currentManualSearchThreadActive = [] - finishedManualSearchThreadItems= [] - - # Queued Searches - currentManualSearchThreadsQueued = sickbeard.searchQueueScheduler.action.get_all_ep_from_queue(show) - # Running Searches - if (sickbeard.searchQueueScheduler.action.is_manualsearch_in_progress()): - currentManualSearchThreadActive = sickbeard.searchQueueScheduler.action.currentItem - - # Finished Searches - finishedManualSearchThreadItems = sickbeard.search_queue.MANUAL_SEARCH_HISTORY - - if currentManualSearchThreadsQueued: - for searchThread in currentManualSearchThreadsQueued: - searchstatus = 'queued' - if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): - episodes.append({'episode': searchThread.segment.episode, - 'episodeindexid': searchThread.segment.indexerid, - 'season' : searchThread.segment.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment)}) - else: - for epObj in searchThread.segment: - episodes.append({'episode': epObj.episode, - 'episodeindexid': epObj.indexerid, - 'season' : epObj.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj)}) - - if currentManualSearchThreadActive: - searchThread = currentManualSearchThreadActive - searchstatus = 'searching' - if searchThread.success: - searchstatus = 'finished' - else: - searchstatus = 'searching' - episodes.append({'episode': searchThread.segment.episode, - 'episodeindexid': searchThread.segment.indexerid, - 'season' : searchThread.segment.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment)}) - - if finishedManualSearchThreadItems: - for searchThread in finishedManualSearchThreadItems: - if isinstance(searchThread, sickbeard.search_queue.ManualSearchQueueItem): - if str(searchThread.show.indexerid) == show and not [x for x in episodes if x['episodeindexid'] == searchThread.segment.indexerid]: - searchstatus = 'finished' - episodes.append({'episode': searchThread.segment.episode, - 'episodeindexid': searchThread.segment.indexerid, - 'season' : searchThread.segment.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[searchThread.segment.status], - 'quality': self.getQualityClass(searchThread.segment)}) - else: - ### These are only Failed Downloads/Retry SearchThreadItems.. lets loop through the segement/episodes - if str(searchThread.show.indexerid) == show: - for epObj in searchThread.segment: - if not [x for x in episodes if x['episodeindexid'] == epObj.indexerid]: - searchstatus = 'finished' - episodes.append({'episode': epObj.episode, - 'episodeindexid': epObj.indexerid, - 'season' : epObj.season, - 'searchstatus' : searchstatus, - 'status' : statusStrings[epObj.status], - 'quality': self.getQualityClass(epObj)}) - - return json.dumps({'show': show, 'episodes' : episodes}) - - #return json.dumps() - - def getQualityClass(self, ep_obj): - # return the correct json value - - # Find the quality class for the episode - quality_class = Quality.qualityStrings[Quality.UNKNOWN] - ep_status, ep_quality = Quality.splitCompositeStatus(ep_obj.status) - for x in (SD, HD720p, HD1080p): - if ep_quality in Quality.splitQuality(x)[0]: - quality_class = qualityPresetStrings[x] - break - - return quality_class - - def searchEpisodeSubtitles(self, show=None, season=None, episode=None): - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # try do download subtitles for that episode - previous_subtitles = set(subliminal.language.Language(x) for x in ep_obj.subtitles) - try: - ep_obj.subtitles = set(x.language for x in ep_obj.downloadSubtitles().values()[0]) - except: - return json.dumps({'result': 'failure'}) - - # return the correct json value - if previous_subtitles != ep_obj.subtitles: - status = 'New subtitles downloaded: %s' % ' '.join([ - "" + x.name + "" for x in - sorted(list(ep_obj.subtitles.difference(previous_subtitles)))]) - else: - status = 'No subtitles downloaded' - ui.notifications.message('Subtitles Search', status) - return json.dumps({'result': status, 'subtitles': ','.join(sorted([x.alpha2 for x in - ep_obj.subtitles.union(previous_subtitles)]))}) - - def setSceneNumbering(self, show, indexer, forSeason=None, forEpisode=None, forAbsolute=None, sceneSeason=None, - sceneEpisode=None, sceneAbsolute=None): - - # sanitize: - if forSeason in ['null', '']: forSeason = None - if forEpisode in ['null', '']: forEpisode = None - if forAbsolute in ['null', '']: forAbsolute = None - if sceneSeason in ['null', '']: sceneSeason = None - if sceneEpisode in ['null', '']: sceneEpisode = None - if sceneAbsolute in ['null', '']: sceneAbsolute = None - - showObj = sickbeard.helpers.findCertainShow(sickbeard.showList, int(show)) - - if showObj.is_anime: - result = { - 'success': True, - 'forAbsolute': forAbsolute, - } - else: - result = { - 'success': True, - 'forSeason': forSeason, - 'forEpisode': forEpisode, - } - - # retrieve the episode object and fail if we can't get one - if showObj.is_anime: - ep_obj = _getEpisode(show, absolute=forAbsolute) - else: - ep_obj = _getEpisode(show, forSeason, forEpisode) - - if isinstance(ep_obj, str): - result['success'] = False - result['errorMessage'] = ep_obj - elif showObj.is_anime: - logger.log(u"setAbsoluteSceneNumbering for %s from %s to %s" % - (show, forAbsolute, sceneAbsolute), logger.DEBUG) - - show = int(show) - indexer = int(indexer) - forAbsolute = int(forAbsolute) - if sceneAbsolute is not None: sceneAbsolute = int(sceneAbsolute) - - set_scene_numbering(show, indexer, absolute_number=forAbsolute, sceneAbsolute=sceneAbsolute) - else: - logger.log(u"setEpisodeSceneNumbering for %s from %sx%s to %sx%s" % - (show, forSeason, forEpisode, sceneSeason, sceneEpisode), logger.DEBUG) - - show = int(show) - indexer = int(indexer) - forSeason = int(forSeason) - forEpisode = int(forEpisode) - if sceneSeason is not None: sceneSeason = int(sceneSeason) - if sceneEpisode is not None: sceneEpisode = int(sceneEpisode) - - set_scene_numbering(show, indexer, season=forSeason, episode=forEpisode, sceneSeason=sceneSeason, - sceneEpisode=sceneEpisode) - - if showObj.is_anime: - sn = get_scene_absolute_numbering(show, indexer, forAbsolute) - if sn: - result['sceneAbsolute'] = sn - else: - result['sceneAbsolute'] = None - else: - sn = get_scene_numbering(show, indexer, forSeason, forEpisode) - if sn: - (result['sceneSeason'], result['sceneEpisode']) = sn - else: - (result['sceneSeason'], result['sceneEpisode']) = (None, None) - - return json.dumps(result) - - def retryEpisode(self, show, season, episode): - - # retrieve the episode object and fail if we can't get one - ep_obj = _getEpisode(show, season, episode) - if isinstance(ep_obj, str): - return json.dumps({'result': 'failure'}) - - # make a queue item for it and put it on the queue - ep_queue_item = search_queue.FailedQueueItem(ep_obj.show, [ep_obj]) - sickbeard.searchQueueScheduler.action.add_item(ep_queue_item) # @UndefinedVariable - - if ep_queue_item.success: - return returnManualSearchResult(ep_queue_item) - if not ep_queue_item.started and ep_queue_item.success is None: - return json.dumps({'result': 'success'}) #I Actually want to call it queued, because the search hasnt been started yet! - if ep_queue_item.started and ep_queue_item.success is None: - return json.dumps({'result': 'success'}) - else: - return json.dumps({'result': 'failure'}) - - @staticmethod - def fetch_releasegroups(show_name): - - if helpers.set_up_anidb_connection(): - try: - anime = adba.Anime(sickbeard.ADBA_CONNECTION, name=show_name) - groups = anime.get_groups() - except Exception, e: - logger.log(u'exception msg: ' + str(e), logger.DEBUG) - return json.dumps({'result': 'fail', 'resp': 'connect'}) - - return json.dumps({'result': 'success', 'groups': groups}) - - return json.dumps({'result': 'fail', 'resp': 'init'}) - - -class UI(MainHandler): - def add_message(self): - ui.notifications.message('Test 1', 'This is test number 1') - ui.notifications.error('Test 2', 'This is test number 2') - - return "ok" - - def get_messages(self): - messages = {} - cur_notification_num = 1 - for cur_notification in ui.notifications.get_notifications(self.request.remote_ip): - messages['notification-' + str(cur_notification_num)] = {'title': cur_notification.title, - 'message': cur_notification.message, - 'type': cur_notification.type} - cur_notification_num += 1 - - return json.dumps(messages) + return t.respond() \ No newline at end of file diff --git a/sickbeard/webserveInit.py b/sickbeard/webserveInit.py index 2d58f2e3..67805582 100644 --- a/sickbeard/webserveInit.py +++ b/sickbeard/webserveInit.py @@ -1,11 +1,8 @@ import os -import socket -import time import threading import sys import sickbeard import webserve -import browser import webapi from sickbeard import logger @@ -90,7 +87,7 @@ class WebServer(threading.Thread): # Main Handler self.app.add_handlers('.*$', [ - (r'%s/api/builder(/?)(.*)' % self.options['web_root'], webapi.ApiBuilder), + (r'%s/api/builder(/?)(.*)' % self.options['web_root'], webserve.ApiBuilder), (r'%s/api(/?.*)' % self.options['web_root'], webapi.Api), (r'%s/config/general(/?.*)' % self.options['web_root'], webserve.ConfigGeneral), (r'%s/config/search(/?.*)' % self.options['web_root'], webserve.ConfigSearch), @@ -109,7 +106,7 @@ class WebServer(threading.Thread): (r'%s/manage/manageSearches(/?.*)' % self.options['web_root'], webserve.ManageSearches), (r'%s/manage/(/?.*)' % self.options['web_root'], webserve.Manage), (r'%s/ui(/?.*)' % self.options['web_root'], webserve.UI), - (r'%s/browser(/?.*)' % self.options['web_root'], browser.WebFileBrowser), + (r'%s/browser(/?.*)' % self.options['web_root'], webserve.WebFileBrowser), (r'%s(/?.*)' % self.options['web_root'], webserve.MainHandler), ]) From 3d4cb6dde49df25be794b25adbf6e0ee1b0f23fb Mon Sep 17 00:00:00 2001 From: JackDandy Date: Mon, 23 Feb 2015 14:01:20 +0000 Subject: [PATCH 05/78] Add text to explain params passed to extra scripts on Config/Post Processing. --- CHANGES.md | 1 + .../default/config_postProcessing.tmpl | 121 ++- gui/slick/js/configPostProcessing.js | 921 +++++++++--------- 3 files changed, 538 insertions(+), 505 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 92b6526f..0455f568 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,7 @@ * Add requirements file for pip (port from midgetspy/sick-beard) * Remove unused libraries fuzzywuzzy and pysrt * Change webserve code to a logical layout and PEP8 +* Add text to explain params passed to extra scripts on Config/Post Processing [develop changelog] diff --git a/gui/slick/interfaces/default/config_postProcessing.tmpl b/gui/slick/interfaces/default/config_postProcessing.tmpl index 11d7af24..7b8eae7b 100644 --- a/gui/slick/interfaces/default/config_postProcessing.tmpl +++ b/gui/slick/interfaces/default/config_postProcessing.tmpl @@ -53,7 +53,7 @@
    must not be the folder where downloading files are created
    - +

    folder where download clients save completed downloads.  note: only use if not using SABnzbd post processing or if SABnzbd is on a different PC to SickGear

    @@ -86,7 +86,7 @@
    @@ -117,17 +117,46 @@ -
    +
    @@ -145,7 +174,7 @@ @@ -155,8 +184,8 @@ @@ -166,7 +195,7 @@ @@ -176,7 +205,7 @@ @@ -186,7 +215,7 @@ @@ -196,14 +225,14 @@ -
    +
    @@ -221,7 +250,7 @@ @@ -243,14 +272,14 @@ @@ -1125,7 +1154,7 @@
    -
    +
    @@ -1202,18 +1231,18 @@ - + #end for -

    +

    -
    +
    -
    +
    All non-absolute folder locations are relative to $sickbeard.DATA_DIR
    - + diff --git a/gui/slick/js/configPostProcessing.js b/gui/slick/js/configPostProcessing.js index 312c0b64..b88546d8 100644 --- a/gui/slick/js/configPostProcessing.js +++ b/gui/slick/js/configPostProcessing.js @@ -1,525 +1,528 @@ $(document).ready(function () { - // http://stackoverflow.com/questions/2219924/idiomatic-jquery-delayed-event-only-after-a-short-pause-in-typing-e-g-timew - var typewatch = (function () { - var timer = 0; - return function (callback, ms) { - clearTimeout(timer); - timer = setTimeout(callback, ms); - }; - })(); + // http://stackoverflow.com/questions/2219924/idiomatic-jquery-delayed-event-only-after-a-short-pause-in-typing-e-g-timew + var typewatch = (function () { + var timer = 0; + return function (callback, ms) { + clearTimeout(timer); + timer = setTimeout(callback, ms); + }; + })(); function israr_supported() { var pattern = $('#naming_pattern').val(); $.get(sbRoot + '/config/postProcessing/isRarSupported', - function (data) { - if (data == "supported") { - } else { - $('#unpack').qtip('option', { - 'content.text': 'Unrar Executable not found.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); + function (data) { + if (data == "supported") { + } else { + $('#unpack').qtip('option', { + 'content.text': 'Unrar Executable not found.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); $('#unpack').qtip('toggle', true); - $('#unpack').css('background-color', '#FFFFDD'); + $('#unpack').css('background-color', '#FFFFDD'); - } - }); + } + }); } - function fill_examples() { - var pattern = $('#naming_pattern').val(); - var multi = $('#naming_multi_ep :selected').val(); - var anime_type = $('input[name="naming_anime"]:checked').val(); + function fill_examples() { + var pattern = $('#naming_pattern').val(); + var multi = $('#naming_multi_ep :selected').val(); + var anime_type = $('input[name="naming_anime"]:checked').val(); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, anime_type: 3}, - function (data) { - if (data) { - $('#naming_example').text(data + '.ext'); - $('#naming_example_div').show(); - } else { - $('#naming_example_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, anime_type: 3}, + function (data) { + if (data) { + $('#naming_example').text(data + '.ext'); + $('#naming_example_div').show(); + } else { + $('#naming_example_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, multi: multi, anime_type: 3}, - function (data) { - if (data) { - $('#naming_example_multi').text(data + '.ext'); - $('#naming_example_multi_div').show(); - } else { - $('#naming_example_multi_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, multi: multi, anime_type: 3}, + function (data) { + if (data) { + $('#naming_example_multi').text(data + '.ext'); + $('#naming_example_multi_div').show(); + } else { + $('#naming_example_multi_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, multi: multi, anime_type: anime_type}, - function (data) { - if (data == "invalid") { - $('#naming_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_pattern').qtip('toggle', true); - $('#naming_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_pattern').qtip('toggle', true); - $('#naming_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'qtip-green qtip-rounded qtip-shadow' - }); - $('#naming_pattern').qtip('toggle', false); - $('#naming_pattern').css('background-color', '#FFFFFF'); - } - }); + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, multi: multi, anime_type: anime_type}, + function (data) { + if (data == "invalid") { + $('#naming_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_pattern').qtip('toggle', true); + $('#naming_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_pattern').qtip('toggle', true); + $('#naming_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'qtip-green qtip-rounded qtip-shadow' + }); + $('#naming_pattern').qtip('toggle', false); + $('#naming_pattern').css('background-color', '#FFFFFF'); + } + }); - } + } - function fill_abd_examples() { - var pattern = $('#naming_abd_pattern').val(); + function fill_abd_examples() { + var pattern = $('#naming_abd_pattern').val(); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, abd: 'True'}, - function (data) { - if (data) { - $('#naming_abd_example').text(data + '.ext'); - $('#naming_abd_example_div').show(); - } else { - $('#naming_abd_example_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, abd: 'True'}, + function (data) { + if (data) { + $('#naming_abd_example').text(data + '.ext'); + $('#naming_abd_example_div').show(); + } else { + $('#naming_abd_example_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, abd: 'True'}, - function (data) { - if (data == "invalid") { - $('#naming_abd_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_abd_pattern').qtip('toggle', true); - $('#naming_abd_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_abd_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_abd_pattern').qtip('toggle', true); - $('#naming_abd_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_abd_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'qtip-green qtip-rounded qtip-shadow' - }); - $('#naming_abd_pattern').qtip('toggle', false); - $('#naming_abd_pattern').css('background-color', '#FFFFFF'); - } - }); + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, abd: 'True'}, + function (data) { + if (data == "invalid") { + $('#naming_abd_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_abd_pattern').qtip('toggle', true); + $('#naming_abd_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_abd_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_abd_pattern').qtip('toggle', true); + $('#naming_abd_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_abd_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'qtip-green qtip-rounded qtip-shadow' + }); + $('#naming_abd_pattern').qtip('toggle', false); + $('#naming_abd_pattern').css('background-color', '#FFFFFF'); + } + }); - } + } - function fill_sports_examples() { - var pattern = $('#naming_sports_pattern').val(); + function fill_sports_examples() { + var pattern = $('#naming_sports_pattern').val(); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, sports: 'True'}, - function (data) { - if (data) { - $('#naming_sports_example').text(data + '.ext'); - $('#naming_sports_example_div').show(); - } else { - $('#naming_sports_example_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, sports: 'True'}, + function (data) { + if (data) { + $('#naming_sports_example').text(data + '.ext'); + $('#naming_sports_example_div').show(); + } else { + $('#naming_sports_example_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, sports: 'True'}, - function (data) { - if (data == "invalid") { - $('#naming_sports_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_sports_pattern').qtip('toggle', true); - $('#naming_sports_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_sports_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_sports_pattern').qtip('toggle', true); - $('#naming_sports_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_sports_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'qtip-green qtip-rounded qtip-shadow' - }); - $('#naming_sports_pattern').qtip('toggle', false); - $('#naming_sports_pattern').css('background-color', '#FFFFFF'); - } - }); + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, sports: 'True'}, + function (data) { + if (data == "invalid") { + $('#naming_sports_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_sports_pattern').qtip('toggle', true); + $('#naming_sports_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_sports_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_sports_pattern').qtip('toggle', true); + $('#naming_sports_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_sports_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'qtip-green qtip-rounded qtip-shadow' + }); + $('#naming_sports_pattern').qtip('toggle', false); + $('#naming_sports_pattern').css('background-color', '#FFFFFF'); + } + }); - } + } - function fill_sports_examples() { - var pattern = $('#naming_sports_pattern').val(); + function fill_sports_examples() { + var pattern = $('#naming_sports_pattern').val(); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, sports: 'True'}, - function (data) { - if (data) { - $('#naming_sports_example').text(data + '.ext'); - $('#naming_sports_example_div').show(); - } else { - $('#naming_sports_example_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, sports: 'True'}, + function (data) { + if (data) { + $('#naming_sports_example').text(data + '.ext'); + $('#naming_sports_example_div').show(); + } else { + $('#naming_sports_example_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, sports: 'True'}, - function (data) { - if (data == "invalid") { - $('#naming_sports_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_sports_pattern').qtip('toggle', true); - $('#naming_sports_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_sports_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_sports_pattern').qtip('toggle', true); - $('#naming_sports_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_sports_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'qtip-green qtip-rounded qtip-shadow' - }); - $('#naming_sports_pattern').qtip('toggle', false); - $('#naming_sports_pattern').css('background-color', '#FFFFFF'); - } - }); + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, sports: 'True'}, + function (data) { + if (data == "invalid") { + $('#naming_sports_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_sports_pattern').qtip('toggle', true); + $('#naming_sports_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_sports_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_sports_pattern').qtip('toggle', true); + $('#naming_sports_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_sports_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'qtip-green qtip-rounded qtip-shadow' + }); + $('#naming_sports_pattern').qtip('toggle', false); + $('#naming_sports_pattern').css('background-color', '#FFFFFF'); + } + }); - } + } - function fill_anime_examples() { - var pattern = $('#naming_anime_pattern').val(); - var multi = $('#naming_anime_multi_ep :selected').val(); - var anime_type = $('input[name="naming_anime"]:checked').val(); + function fill_anime_examples() { + var pattern = $('#naming_anime_pattern').val(); + var multi = $('#naming_anime_multi_ep :selected').val(); + var anime_type = $('input[name="naming_anime"]:checked').val(); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, anime_type: anime_type}, - function (data) { - if (data) { - $('#naming_example_anime').text(data + '.ext'); - $('#naming_example_anime_div').show(); - } else { - $('#naming_example_anime_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, anime_type: anime_type}, + function (data) { + if (data) { + $('#naming_example_anime').text(data + '.ext'); + $('#naming_example_anime_div').show(); + } else { + $('#naming_example_anime_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, multi: multi, anime_type: anime_type}, - function (data) { - if (data) { - $('#naming_example_multi_anime').text(data + '.ext'); - $('#naming_example_multi_anime_div').show(); - } else { - $('#naming_example_multi_anime_div').hide(); - } - }); + $.get(sbRoot + '/config/postProcessing/testNaming', {pattern: pattern, multi: multi, anime_type: anime_type}, + function (data) { + if (data) { + $('#naming_example_multi_anime').text(data + '.ext'); + $('#naming_example_multi_anime_div').show(); + } else { + $('#naming_example_multi_anime_div').hide(); + } + }); - $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, multi: multi, anime_type: anime_type}, - function (data) { - if (data == "invalid") { - $('#naming_anime_pattern').qtip('option', { - 'content.text': 'This pattern is invalid.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_anime_pattern').qtip('toggle', true); - $('#naming_anime_pattern').css('background-color', '#FFDDDD'); - } else if (data == "seasonfolders") { - $('#naming_anime_pattern').qtip('option', { - 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', - 'style.classes': 'qtip-red qtip-rounded qtip-shadow' - }); - $('#naming_anime_pattern').qtip('toggle', true); - $('#naming_anime_pattern').css('background-color', '#FFFFDD'); - } else { - $('#naming_anime_pattern').qtip('option', { - 'content.text': 'This pattern is valid.', - 'style.classes': 'qtip-green qtip-rounded qtip-shadow' - }); - $('#naming_anime_pattern').qtip('toggle', false); - $('#naming_anime_pattern').css('background-color', '#FFFFFF'); - } - }); - } + $.get(sbRoot + '/config/postProcessing/isNamingValid', {pattern: pattern, multi: multi, anime_type: anime_type}, + function (data) { + if (data == "invalid") { + $('#naming_anime_pattern').qtip('option', { + 'content.text': 'This pattern is invalid.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_anime_pattern').qtip('toggle', true); + $('#naming_anime_pattern').css('background-color', '#FFDDDD'); + } else if (data == "seasonfolders") { + $('#naming_anime_pattern').qtip('option', { + 'content.text': 'This pattern would be invalid without the folders, using it will force "Flatten" off for all shows.', + 'style.classes': 'qtip-red qtip-rounded qtip-shadow' + }); + $('#naming_anime_pattern').qtip('toggle', true); + $('#naming_anime_pattern').css('background-color', '#FFFFDD'); + } else { + $('#naming_anime_pattern').qtip('option', { + 'content.text': 'This pattern is valid.', + 'style.classes': 'qtip-green qtip-rounded qtip-shadow' + }); + $('#naming_anime_pattern').qtip('toggle', false); + $('#naming_anime_pattern').css('background-color', '#FFFFFF'); + } + }); + } - function setup_naming() { - // if it is a custom selection then show the text box - if ($('#name_presets :selected').val() == "Custom...") { - $('#naming_custom').show(); - } else { - $('#naming_custom').hide(); - $('#naming_pattern').val($('#name_presets :selected').attr('id')); - } - fill_examples(); - } - - function setup_abd_naming() { - // if it is a custom selection then show the text box - if ($('#name_abd_presets :selected').val() == "Custom...") { - $('#naming_abd_custom').show(); - } else { - $('#naming_abd_custom').hide(); - $('#naming_abd_pattern').val($('#name_abd_presets :selected').attr('id')); - } - fill_abd_examples(); - } - - function setup_sports_naming() { - // if it is a custom selection then show the text box - if ($('#name_sports_presets :selected').val() == "Custom...") { - $('#naming_sports_custom').show(); - } else { - $('#naming_sports_custom').hide(); - $('#naming_sports_pattern').val($('#name_sports_presets :selected').attr('id')); - } - fill_sports_examples(); - } - - function setup_anime_naming() { - // if it is a custom selection then show the text box - if ($('#name_anime_presets :selected').val() == "Custom...") { - $('#naming_anime_custom').show(); - } else { - $('#naming_anime_custom').hide(); - $('#naming_anime_pattern').val($('#name_anime_presets :selected').attr('id')); - } - fill_anime_examples(); - } - - $('#unpack').change(function () { - if(this.checked) { - israr_supported(); - } else { - $('#unpack').qtip('toggle', false); + function setup_naming() { + // if it is a custom selection then show the text box + if ($('#name_presets :selected').val() == "Custom...") { + $('#naming_custom').show(); + } else { + $('#naming_custom').hide(); + $('#naming_pattern').val($('#name_presets :selected').attr('id')); } - }); + fill_examples(); + } - $('#name_presets').change(function () { - setup_naming(); - }); + function setup_abd_naming() { + // if it is a custom selection then show the text box + if ($('#name_abd_presets :selected').val() == "Custom...") { + $('#naming_abd_custom').show(); + } else { + $('#naming_abd_custom').hide(); + $('#naming_abd_pattern').val($('#name_abd_presets :selected').attr('id')); + } + fill_abd_examples(); + } - $('#name_abd_presets').change(function () { - setup_abd_naming(); - }); + function setup_sports_naming() { + // if it is a custom selection then show the text box + if ($('#name_sports_presets :selected').val() == "Custom...") { + $('#naming_sports_custom').show(); + } else { + $('#naming_sports_custom').hide(); + $('#naming_sports_pattern').val($('#name_sports_presets :selected').attr('id')); + } + fill_sports_examples(); + } - $('#naming_custom_abd').change(function () { - setup_abd_naming(); - }); + function setup_anime_naming() { + // if it is a custom selection then show the text box + if ($('#name_anime_presets :selected').val() == "Custom...") { + $('#naming_anime_custom').show(); + } else { + $('#naming_anime_custom').hide(); + $('#naming_anime_pattern').val($('#name_anime_presets :selected').attr('id')); + } + fill_anime_examples(); + } - $('#name_sports_presets').change(function () { - setup_sports_naming(); - }); + $('#unpack').change(function () { + if(this.checked) { + israr_supported(); + } else { + $('#unpack').qtip('toggle', false); + } + }); - $('#naming_custom_sports').change(function () { - setup_sports_naming(); - }); + $('#name_presets').change(function () { + setup_naming(); + }); - $('#name_anime_presets').change(function () { - setup_anime_naming(); - }); + $('#name_abd_presets').change(function () { + setup_abd_naming(); + }); - $('#naming_custom_anime').change(function () { - setup_anime_naming(); - }); + $('#naming_custom_abd').change(function () { + setup_abd_naming(); + }); - $('input[name="naming_anime"]').click(function(){ - setup_anime_naming(); - }); + $('#name_sports_presets').change(function () { + setup_sports_naming(); + }); - $('#naming_multi_ep').change(fill_examples); - $('#naming_pattern').focusout(fill_examples); - $('#naming_pattern').keyup(function () { - typewatch(function () { - fill_examples(); - }, 500); - }); + $('#naming_custom_sports').change(function () { + setup_sports_naming(); + }); - $('#naming_anime_multi_ep').change(fill_anime_examples); - $('#naming_anime_pattern').focusout(fill_anime_examples); - $('#naming_anime_pattern').keyup(function () { - typewatch(function () { - fill_anime_examples(); - }, 500); - }); + $('#name_anime_presets').change(function () { + setup_anime_naming(); + }); - $('#naming_abd_pattern').focusout(fill_examples); - $('#naming_abd_pattern').keyup(function () { - typewatch(function () { - fill_abd_examples(); - }, 500); - }); + $('#naming_custom_anime').change(function () { + setup_anime_naming(); + }); - $('#naming_sports_pattern').focusout(fill_examples); - $('#naming_sports_pattern').keyup(function () { - typewatch(function () { - fill_sports_examples(); - }, 500); - }); + $('input[name="naming_anime"]').click(function(){ + setup_anime_naming(); + }); - $('#naming_anime_pattern').focusout(fill_examples); - $('#naming_anime_pattern').keyup(function () { - typewatch(function () { - fill_anime_examples(); - }, 500); - }); + $('#naming_multi_ep').change(fill_examples); + $('#naming_pattern').focusout(fill_examples); + $('#naming_pattern').keyup(function () { + typewatch(function () { + fill_examples(); + }, 500); + }); - $('#show_naming_key').click(function () { - $('#naming_key').toggle(); - }); - $('#show_naming_abd_key').click(function () { - $('#naming_abd_key').toggle(); - }); - $('#show_naming_sports_key').click(function () { - $('#naming_sports_key').toggle(); - }); - $('#show_naming_anime_key').click(function () { - $('#naming_anime_key').toggle(); - }); - $('#do_custom').click(function () { - $('#naming_pattern').val($('#name_presets :selected').attr('id')); - $('#naming_custom').show(); - $('#naming_pattern').focus(); - }); - setup_naming(); - setup_abd_naming(); - setup_sports_naming(); - setup_anime_naming(); + $('#naming_anime_multi_ep').change(fill_anime_examples); + $('#naming_anime_pattern').focusout(fill_anime_examples); + $('#naming_anime_pattern').keyup(function () { + typewatch(function () { + fill_anime_examples(); + }, 500); + }); + + $('#naming_abd_pattern').focusout(fill_examples); + $('#naming_abd_pattern').keyup(function () { + typewatch(function () { + fill_abd_examples(); + }, 500); + }); + + $('#naming_sports_pattern').focusout(fill_examples); + $('#naming_sports_pattern').keyup(function () { + typewatch(function () { + fill_sports_examples(); + }, 500); + }); + + $('#naming_anime_pattern').focusout(fill_examples); + $('#naming_anime_pattern').keyup(function () { + typewatch(function () { + fill_anime_examples(); + }, 500); + }); + + $('#show_extra_params').click(function () { + $('#extra_params').toggle(); + }); + $('#show_naming_key').click(function () { + $('#naming_key').toggle(); + }); + $('#show_naming_abd_key').click(function () { + $('#naming_abd_key').toggle(); + }); + $('#show_naming_sports_key').click(function () { + $('#naming_sports_key').toggle(); + }); + $('#show_naming_anime_key').click(function () { + $('#naming_anime_key').toggle(); + }); + $('#do_custom').click(function () { + $('#naming_pattern').val($('#name_presets :selected').attr('id')); + $('#naming_custom').show(); + $('#naming_pattern').focus(); + }); + setup_naming(); + setup_abd_naming(); + setup_sports_naming(); + setup_anime_naming(); - // -- start of metadata options div toggle code -- - $('#metadataType').on('change keyup', function () { - $(this).showHideMetadata(); - }); + // -- start of metadata options div toggle code -- + $('#metadataType').on('change keyup', function () { + $(this).showHideMetadata(); + }); - $.fn.showHideMetadata = function () { - $('.metadataDiv').each(function () { - var targetName = $(this).attr('id'); - var selectedTarget = $('#metadataType :selected').val(); + $.fn.showHideMetadata = function () { + $('.metadataDiv').each(function () { + var targetName = $(this).attr('id'); + var selectedTarget = $('#metadataType :selected').val(); - if (selectedTarget == targetName) { - $(this).show(); - } else { - $(this).hide(); - } - }); - }; - //initialize to show the div - $(this).showHideMetadata(); - // -- end of metadata options div toggle code -- + if (selectedTarget == targetName) { + $(this).show(); + } else { + $(this).hide(); + } + }); + }; + //initialize to show the div + $(this).showHideMetadata(); + // -- end of metadata options div toggle code -- - $('.metadata_checkbox').click(function () { - $(this).refreshMetadataConfig(false); - }); + $('.metadata_checkbox').click(function () { + $(this).refreshMetadataConfig(false); + }); - $.fn.refreshMetadataConfig = function (first) { + $.fn.refreshMetadataConfig = function (first) { - var cur_most = 0; - var cur_most_provider = ''; + var cur_most = 0; + var cur_most_provider = ''; - $('.metadataDiv').each(function () { - var generator_name = $(this).attr('id'); + $('.metadataDiv').each(function () { + var generator_name = $(this).attr('id'); - var config_arr = []; - var show_metadata = $("#" + generator_name + "_show_metadata").prop('checked'); - var episode_metadata = $("#" + generator_name + "_episode_metadata").prop('checked'); - var fanart = $("#" + generator_name + "_fanart").prop('checked'); - var poster = $("#" + generator_name + "_poster").prop('checked'); - var banner = $("#" + generator_name + "_banner").prop('checked'); - var episode_thumbnails = $("#" + generator_name + "_episode_thumbnails").prop('checked'); - var season_posters = $("#" + generator_name + "_season_posters").prop('checked'); - var season_banners = $("#" + generator_name + "_season_banners").prop('checked'); - var season_all_poster = $("#" + generator_name + "_season_all_poster").prop('checked'); - var season_all_banner = $("#" + generator_name + "_season_all_banner").prop('checked'); + var config_arr = []; + var show_metadata = $("#" + generator_name + "_show_metadata").prop('checked'); + var episode_metadata = $("#" + generator_name + "_episode_metadata").prop('checked'); + var fanart = $("#" + generator_name + "_fanart").prop('checked'); + var poster = $("#" + generator_name + "_poster").prop('checked'); + var banner = $("#" + generator_name + "_banner").prop('checked'); + var episode_thumbnails = $("#" + generator_name + "_episode_thumbnails").prop('checked'); + var season_posters = $("#" + generator_name + "_season_posters").prop('checked'); + var season_banners = $("#" + generator_name + "_season_banners").prop('checked'); + var season_all_poster = $("#" + generator_name + "_season_all_poster").prop('checked'); + var season_all_banner = $("#" + generator_name + "_season_all_banner").prop('checked'); - config_arr.push(show_metadata ? '1' : '0'); - config_arr.push(episode_metadata ? '1' : '0'); - config_arr.push(fanart ? '1' : '0'); + config_arr.push(show_metadata ? '1' : '0'); + config_arr.push(episode_metadata ? '1' : '0'); + config_arr.push(fanart ? '1' : '0'); config_arr.push(poster ? '1' : '0'); config_arr.push(banner ? '1' : '0'); - config_arr.push(episode_thumbnails ? '1' : '0'); - config_arr.push(season_posters ? '1' : '0'); - config_arr.push(season_banners ? '1' : '0'); - config_arr.push(season_all_poster ? '1' : '0'); - config_arr.push(season_all_banner ? '1' : '0'); + config_arr.push(episode_thumbnails ? '1' : '0'); + config_arr.push(season_posters ? '1' : '0'); + config_arr.push(season_banners ? '1' : '0'); + config_arr.push(season_all_poster ? '1' : '0'); + config_arr.push(season_all_banner ? '1' : '0'); - var cur_num = 0; - for (var i = 0; i < config_arr.length; i++) { - cur_num += parseInt(config_arr[i]); - } - if (cur_num > cur_most) { - cur_most = cur_num; - cur_most_provider = generator_name; - } + var cur_num = 0; + for (var i = 0; i < config_arr.length; i++) { + cur_num += parseInt(config_arr[i]); + } + if (cur_num > cur_most) { + cur_most = cur_num; + cur_most_provider = generator_name; + } - $("#" + generator_name + "_eg_show_metadata").attr('class', show_metadata ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_episode_metadata").attr('class', episode_metadata ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_fanart").attr('class', fanart ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_poster").attr('class', poster ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_banner").attr('class', banner ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_episode_thumbnails").attr('class', episode_thumbnails ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_season_posters").attr('class', season_posters ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_season_banners").attr('class', season_banners ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_season_all_poster").attr('class', season_all_poster ? 'enabled' : 'disabled'); - $("#" + generator_name + "_eg_season_all_banner").attr('class', season_all_banner ? 'enabled' : 'disabled'); - $("#" + generator_name + "_data").val(config_arr.join('|')); + $("#" + generator_name + "_eg_show_metadata").attr('class', show_metadata ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_episode_metadata").attr('class', episode_metadata ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_fanart").attr('class', fanart ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_poster").attr('class', poster ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_banner").attr('class', banner ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_episode_thumbnails").attr('class', episode_thumbnails ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_season_posters").attr('class', season_posters ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_season_banners").attr('class', season_banners ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_season_all_poster").attr('class', season_all_poster ? 'enabled' : 'disabled'); + $("#" + generator_name + "_eg_season_all_banner").attr('class', season_all_banner ? 'enabled' : 'disabled'); + $("#" + generator_name + "_data").val(config_arr.join('|')); - }); + }); - if (cur_most_provider != '' && first) { - $('#metadataType option[value=' + cur_most_provider + ']').attr('selected', 'selected'); - $(this).showHideMetadata(); - } + if (cur_most_provider != '' && first) { + $('#metadataType option[value=' + cur_most_provider + ']').attr('selected', 'selected'); + $(this).showHideMetadata(); + } - } + } - $(this).refreshMetadataConfig(true); - $('img[title]').qtip({ - position: { - viewport: $(window), - my: 'top right', - at: 'bottom center' - }, - style: { - classes: 'qtip-dark qtip-rounded qtip-shadow' - } - }); - $('i[title]').qtip({ - position: { - viewport: $(window), - my: 'bottom center', - at: 'top center' - }, - style: { - classes: 'qtip-rounded qtip-shadow' - } - }); - $('.custom-pattern,#unpack').qtip({ - content: 'validating...', - show: { - event: false, - ready: false - }, - hide: false, - position: { - viewport: $(window), - my: 'right center', - at: 'left center' - }, - style: { - classes: 'qtip-red qtip-rounded qtip-shadow' - } - }); + $(this).refreshMetadataConfig(true); + $('img[title]').qtip({ + position: { + viewport: $(window), + my: 'top right', + at: 'bottom center' + }, + style: { + classes: 'qtip-dark qtip-rounded qtip-shadow' + } + }); + $('i[title]').qtip({ + position: { + viewport: $(window), + my: 'bottom center', + at: 'top center' + }, + style: { + classes: 'qtip-rounded qtip-shadow' + } + }); + $('.custom-pattern,#unpack').qtip({ + content: 'validating...', + show: { + event: false, + ready: false + }, + hide: false, + position: { + viewport: $(window), + my: 'right center', + at: 'left center' + }, + style: { + classes: 'qtip-red qtip-rounded qtip-shadow' + } + }); }); \ No newline at end of file From 87d46b2c3f041807974eb18dd50e18ac4ab2dd00 Mon Sep 17 00:00:00 2001 From: Adam Date: Tue, 24 Feb 2015 15:01:12 +0800 Subject: [PATCH 06/78] Remove unused SickBeardURLOpener and AuthURLOpener classes --- CHANGES.md | 1 + sickbeard/classes.py | 51 ++------------------------------------------ sickbeard/helpers.py | 2 -- 3 files changed, 3 insertions(+), 51 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 0455f568..59c1d32e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -4,6 +4,7 @@ * Remove unused libraries fuzzywuzzy and pysrt * Change webserve code to a logical layout and PEP8 * Add text to explain params passed to extra scripts on Config/Post Processing +* Remove unused SickBeardURLOpener and AuthURLOpener classes [develop changelog] diff --git a/sickbeard/classes.py b/sickbeard/classes.py index 0094ba08..7cc56bee 100644 --- a/sickbeard/classes.py +++ b/sickbeard/classes.py @@ -16,58 +16,11 @@ # You should have received a copy of the GNU General Public License # along with SickGear. If not, see . import re - +import datetime import sickbeard -import urllib -import datetime from lib.dateutil import parser - -from common import USER_AGENT, Quality - - -class SickBeardURLopener(urllib.FancyURLopener): - version = USER_AGENT - - -class AuthURLOpener(SickBeardURLopener): - """ - URLOpener class that supports http auth without needing interactive password entry. - If the provided username/password don't work it simply fails. - - user: username to use for HTTP auth - pw: password to use for HTTP auth - """ - - def __init__(self, user, pw): - self.username = user - self.password = pw - - # remember if we've tried the username/password before - self.numTries = 0 - - # call the base class - urllib.FancyURLopener.__init__(self) - - def prompt_user_passwd(self, host, realm): - """ - Override this function and instead of prompting just give the - username/password that were provided when the class was instantiated. - """ - - # if this is the first try then provide a username/password - if self.numTries == 0: - self.numTries = 1 - return (self.username, self.password) - - # if we've tried before then return blank which cancels the request - else: - return ('', '') - - # this is pretty much just a hack for convenience - def openit(self, url): - self.numTries = 0 - return SickBeardURLopener.open(self, url) +from sickbeard.common import Quality class SearchResult: diff --git a/sickbeard/helpers.py b/sickbeard/helpers.py index 6ceb914c..d723d77e 100644 --- a/sickbeard/helpers.py +++ b/sickbeard/helpers.py @@ -65,8 +65,6 @@ from sickbeard import clients from lib.cachecontrol import CacheControl, caches from itertools import izip, cycle -urllib._urlopener = classes.SickBeardURLopener() - def indentXML(elem, level=0): ''' From 2b031fff67a9d593c9f670192bf5fa883fa7b118 Mon Sep 17 00:00:00 2001 From: adam Date: Fri, 20 Feb 2015 22:03:27 +0800 Subject: [PATCH 07/78] Change startup code cleanup and PEP8 --- CHANGES.md | 1 + SickBeard.py | 135 ++++++++++++++++++++++++++------------------------- 2 files changed, 69 insertions(+), 67 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 76aefc66..4912a7da 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -6,6 +6,7 @@ * Add text to explain params passed to extra scripts on Config/Post Processing * Remove unused SickBeardURLOpener and AuthURLOpener classes * Update Pushbullet notifier (port from midgetspy/sickbeard) +* Change startup code cleanup and PEP8 [develop changelog] diff --git a/SickBeard.py b/SickBeard.py index d9522c57..df572169 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -25,9 +25,14 @@ import signal import sys import shutil import subprocess +import os +import locale +import datetime +import threading +import getopt if sys.version_info < (2, 6): - print "Sorry, requires Python 2.6 or 2.7." + print 'Sorry, requires Python 2.6 or 2.7.' sys.exit(1) try: @@ -36,27 +41,20 @@ try: if Cheetah.Version[0] != '2': raise ValueError except ValueError: - print "Sorry, requires Python module Cheetah 2.1.0 or newer." + print 'Sorry, requires Python module Cheetah 2.1.0 or newer.' sys.exit(1) except: - print "The Python module Cheetah is required" + print 'The Python module Cheetah is required' sys.exit(1) -import os - sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib'))) # We only need this for compiling an EXE and I will just always do that on 2.6+ if sys.hexversion >= 0x020600F0: from multiprocessing import freeze_support # @UnresolvedImport -import locale -import datetime -import threading -import getopt - import sickbeard -from sickbeard import db, logger, network_timezones, failed_history, name_cache, versionChecker +from sickbeard import db, logger, network_timezones, failed_history, name_cache from sickbeard.tv import TVShow from sickbeard.webserveInit import WebServer from sickbeard.databases.mainDB import MIN_DB_VERSION, MAX_DB_VERSION @@ -85,35 +83,36 @@ class SickGear(object): self.forcedPort = None self.noLaunch = False - def help_message(self): + @staticmethod + def help_message(): """ print help message for commandline options """ - help_msg = "\n" - help_msg += "Usage: " + sickbeard.MY_FULLNAME + "