diff --git a/.pullapprove.yml b/.pullapprove.yml index f4e7046f97..4265923062 100644 --- a/.pullapprove.yml +++ b/.pullapprove.yml @@ -4,40 +4,14 @@ reject_regex: (^Rejected|^Fix it) reset_on_push: false reviewers: - - name: backend-devs + name: devs required: 1 members: - duramato - fernandog - labrys - medariox + - ratoaq2 - p0psicles - - adaur -# # CONDITIONS REQUIRE PRO ACCOUNT -# conditions: -# branches: -# - master -# - beta -# - develop -# files: -# - "*.py" - - - name: gui-devs - required: 1 - members: - - Labrys - OmgImAlexis - - p0psicles -# # CONDITIONS REQUIRE PRO ACCOUNT -# conditions: -# branches: -# - master -# - beta -# - develop -# files: -# - "gui/" - - - name: support - required: 0 - members: - - neoatomic + - adaur diff --git a/SickBeard.py b/SickBeard.py index 7bea88e506..936d2b71b3 100755 --- a/SickBeard.py +++ b/SickBeard.py @@ -43,8 +43,8 @@ is installed """ -from __future__ import unicode_literals from __future__ import print_function +from __future__ import unicode_literals import codecs import datetime @@ -94,7 +94,7 @@ import sickbeard from sickbeard import db, logger, network_timezones, failed_history, name_cache from sickbeard.tv import TVShow -from sickbeard.webserveInit import SRWebServer +from sickbeard.server.core import SRWebServer from sickbeard.event_queue import Events from configobj import ConfigObj # pylint: disable=import-error diff --git a/gui/slick/images/manualsearch-white.png b/gui/slick/images/manualsearch-white.png new file mode 100644 index 0000000000..6ab23b027b Binary files /dev/null and b/gui/slick/images/manualsearch-white.png differ diff --git a/gui/slick/images/providers/bithdtv.png b/gui/slick/images/providers/bithdtv.png new file mode 100644 index 0000000000..aecf0ed6b1 Binary files /dev/null and b/gui/slick/images/providers/bithdtv.png differ diff --git a/gui/slick/images/providers/zooqle.png b/gui/slick/images/providers/zooqle.png new file mode 100644 index 0000000000..5a9c435215 Binary files /dev/null and b/gui/slick/images/providers/zooqle.png differ diff --git a/gui/slick/views/config_general.mako b/gui/slick/views/config_general.mako index cbc7302e37..d6bd1fde34 100644 --- a/gui/slick/views/config_general.mako +++ b/gui/slick/views/config_general.mako @@ -4,7 +4,7 @@ import locale import sickbeard from sickbeard.common import SKIPPED, WANTED, UNAIRED, ARCHIVED, IGNORED, SNATCHED, SNATCHED_PROPER, SNATCHED_BEST, FAILED - from sickbeard.common import Quality, qualityPresets, statusStrings, qualityPresetStrings, cpu_presets + from sickbeard.common import Quality, qualityPresets, statusStrings, qualityPresetStrings, cpu_presets, privacy_levels from sickbeard.sbdatetime import sbdatetime, date_presets, time_presets from sickbeard import config from sickbeard import metadata @@ -526,26 +526,6 @@ -
+ + + History |
+ |||||||||
---|---|---|---|---|---|---|---|---|---|
- - - History |
- |||||||||
Date | +Status | +Provider/Group | +Release | +||||||
Date | -Status | -Provider/Group | -Release | -||||||
- <% action_date = sbdatetime.sbfdatetime(datetime.datetime.strptime(str(item['date']), History.date_format), show_seconds=True) %> - ${action_date} - | -- ${statusStrings[status]} ${renderQualityPill(quality)} - | -- <% provider = providers.getProviderClass(GenericProvider.make_id(item["provider"])) %> - % if provider is not None: - ${item["provider"]} - % else: - ${item['provider'] if item['provider'] != "-1" else 'Unknown'} + | |||||||
- ${os.path.basename(item['resource'])} - | -
{error}
+{trace}
+{request}
+ + + + """.format(title=error, error=error, trace=trace_info, request=request_info, root=sickbeard.WEB_ROOT) + ) + + def redirect(self, url, permanent=False, status=None): + """ + Sends a redirect to the given (optionally relative) URL. + + ----->>>>> NOTE: Removed self.finish <<<<<----- + + If the ``status`` argument is specified, that value is used as the + HTTP status code; otherwise either 301 (permanent) or 302 + (temporary) is chosen based on the ``permanent`` argument. + The default is 302 (temporary). + """ + if not url.startswith(sickbeard.WEB_ROOT): + url = sickbeard.WEB_ROOT + url + + if self._headers_written: + raise Exception('Cannot redirect after headers have been written') + if status is None: + status = 301 if permanent else 302 + else: + assert isinstance(status, int) + assert 300 <= status <= 399 + self.set_status(status) + self.set_header('Location', urljoin(utf8(self.request.uri), + utf8(url))) + + def get_current_user(self): + if not isinstance(self, UI) and sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD: + return self.get_secure_cookie('sickrage_user') + else: + return True + + +class WebHandler(BaseHandler): + """ + Base Handler for the web server + """ + def __init__(self, *args, **kwargs): + super(WebHandler, self).__init__(*args, **kwargs) + self.io_loop = IOLoop.current() + + executor = ThreadPoolExecutor(cpu_count()) + + @authenticated + @coroutine + def get(self, route, *args, **kwargs): + try: + # route -> method obj + route = route.strip('/').replace('.', '_') or 'index' + method = getattr(self, route) + + results = yield self.async_call(method) + self.finish(results) + + except Exception: + logger.log(u'Failed doing web ui request {route!r}: {error}'.format + (route=route, error=traceback.format_exc()), logger.DEBUG) + raise HTTPError(404) + + @run_on_executor + def async_call(self, function): + try: + kwargs = self.request.arguments + for arg, value in kwargs.iteritems(): + if len(value) == 1: + kwargs[arg] = value[0] + + result = function(**kwargs) + return result + except Exception: + logger.log(u'Failed doing web ui callback: {error}'.format(error=traceback.format_exc()), logger.ERROR) + raise + + # post uses get method + post = get + + +@route('(.*)(/?)') +class WebRoot(WebHandler): + """ + Base Handler for the web server + """ + def __init__(self, *args, **kwargs): + super(WebRoot, self).__init__(*args, **kwargs) + + def index(self): + return self.redirect('/{page}/'.format(page=sickbeard.DEFAULT_PAGE)) + + def robots_txt(self): + """ Keep web crawlers out """ + self.set_header('Content-Type', 'text/plain') + return 'User-agent: *\nDisallow: /' + + def apibuilder(self): + def titler(x): + return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] + + main_db_con = db.DBConnection(row_type='dict') + shows = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) + episodes = {} + + results = main_db_con.select( + b'SELECT episode, season, showid ' + b'FROM tv_episodes ' + b'ORDER BY season ASC, episode ASC' + ) + + for result in results: + if result[b'showid'] not in episodes: + episodes[result[b'showid']] = {} + + if result[b'season'] not in episodes[result[b'showid']]: + episodes[result[b'showid']][result[b'season']] = [] + + episodes[result[b'showid']][result[b'season']].append(result[b'episode']) + + if len(sickbeard.API_KEY) == 32: + apikey = sickbeard.API_KEY + else: + apikey = 'API Key not generated' + + t = PageTemplate(rh=self, filename='apiBuilder.mako') + return t.render(title='API Builder', header='API Builder', shows=shows, episodes=episodes, apikey=apikey, + commands=function_mapper) + + def showPoster(self, show=None, which=None): + media = None + media_format = ('normal', 'thumb')[which in ('banner_thumb', 'poster_thumb', 'small')] + + if which[0:6] == 'banner': + media = ShowBanner(show, media_format) + elif which[0:6] == 'fanart': + media = ShowFanArt(show, media_format) + elif which[0:6] == 'poster': + media = ShowPoster(show, media_format) + elif which[0:7] == 'network': + media = ShowNetworkLogo(show, media_format) + + if media is not None: + self.set_header('Content-Type', media.get_media_type()) + + return media.get_media() + + return None + + def setHomeLayout(self, layout): + + if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'): + layout = 'poster' + + sickbeard.HOME_LAYOUT = layout + # Don't redirect to default page so user can see new layout + return self.redirect('/home/') + + @staticmethod + def setPosterSortBy(sort): + if sort not in ('name', 'date', 'network', 'progress'): + sort = 'name' + + sickbeard.POSTER_SORTBY = sort + sickbeard.save_config() + + @staticmethod + def setPosterSortDir(direction): + + sickbeard.POSTER_SORTDIR = int(direction) + sickbeard.save_config() + + def setHistoryLayout(self, layout): + + if layout not in ('compact', 'detailed'): + layout = 'detailed' + + sickbeard.HISTORY_LAYOUT = layout + + return self.redirect('/history/') + + def toggleDisplayShowSpecials(self, show): + + sickbeard.DISPLAY_SHOW_SPECIALS = not sickbeard.DISPLAY_SHOW_SPECIALS + + return self.redirect('/home/displayShow?show={show}'.format(show=show)) + + def setScheduleLayout(self, layout): + if layout not in ('poster', 'banner', 'list', 'calendar'): + layout = 'banner' + + if layout == 'calendar': + sickbeard.COMING_EPS_SORT = 'date' + + sickbeard.COMING_EPS_LAYOUT = layout + + return self.redirect('/schedule/') + + def toggleScheduleDisplayPaused(self): + + sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED + + return self.redirect('/schedule/') + + def setScheduleSort(self, sort): + if sort not in ('date', 'network', 'show'): + sort = 'date' + + if sickbeard.COMING_EPS_LAYOUT == 'calendar': + sort \ + = 'date' + + sickbeard.COMING_EPS_SORT = sort + + return self.redirect('/schedule/') + + def schedule(self, layout=None): + next_week = datetime.date.today() + datetime.timedelta(days=7) + next_week1 = datetime.datetime.combine(next_week, datetime.time(tzinfo=network_timezones.sb_timezone)) + results = ComingEpisodes.get_coming_episodes(ComingEpisodes.categories, sickbeard.COMING_EPS_SORT, False) + today = datetime.datetime.now().replace(tzinfo=network_timezones.sb_timezone) + + submenu = [ + { + 'title': 'Sort by:', + 'path': { + 'Date': 'setScheduleSort/?sort=date', + 'Show': 'setScheduleSort/?sort=show', + 'Network': 'setScheduleSort/?sort=network', + } + }, + { + 'title': 'Layout:', + 'path': { + 'Banner': 'setScheduleLayout/?layout=banner', + 'Poster': 'setScheduleLayout/?layout=poster', + 'List': 'setScheduleLayout/?layout=list', + 'Calendar': 'setScheduleLayout/?layout=calendar', + } + }, + { + 'title': 'View Paused:', + 'path': { + 'Hide': 'toggleScheduleDisplayPaused' + } if sickbeard.COMING_EPS_DISPLAY_PAUSED else { + 'Show': 'toggleScheduleDisplayPaused' + } + }, + ] + + # Allow local overriding of layout parameter + if layout and layout in ('poster', 'banner', 'list', 'calendar'): + layout = layout + else: + layout = sickbeard.COMING_EPS_LAYOUT + + t = PageTemplate(rh=self, filename='schedule.mako') + return t.render(submenu=submenu, next_week=next_week1, today=today, results=results, layout=layout, + title='Schedule', header='Schedule', topmenu='schedule', + controller='schedule', action='index') + + +@route('/ui(/?.*)') +class UI(WebRoot): + def __init__(self, *args, **kwargs): + super(UI, self).__init__(*args, **kwargs) + + @staticmethod + def add_message(): + 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): + self.set_header('Cache-Control', 'max-age=0,no-cache,no-store') + self.set_header('Content-Type', 'application/json') + messages = {} + cur_notification_num = 1 + for cur_notification in ui.notifications.get_notifications(self.request.remote_ip): + messages['notification-{number}'.format(number=cur_notification_num)] = { + 'title': cur_notification.title, + 'message': cur_notification.message, + 'type': cur_notification.type, + } + cur_notification_num += 1 + + return json.dumps(messages) diff --git a/sickbeard/server/web/core/calendar.py b/sickbeard/server/web/core/calendar.py new file mode 100644 index 0000000000..f073066a8a --- /dev/null +++ b/sickbeard/server/web/core/calendar.py @@ -0,0 +1,121 @@ +# coding=utf-8 + +""" +iCalendar (iCal) - Standard RFC 5545
+ 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. +
+%s
-%s
-%s
- - - - """ % (error, error, trace_info, request_info, sickbeard.WEB_ROOT) - ) - - def redirect(self, url, permanent=False, status=None): - """Sends a redirect to the given (optionally relative) URL. - - ----->>>>> NOTE: Removed self.finish <<<<<----- - - If the ``status`` argument is specified, that value is used as the - HTTP status code; otherwise either 301 (permanent) or 302 - (temporary) is chosen based on the ``permanent`` argument. - The default is 302 (temporary). - """ - if not url.startswith(sickbeard.WEB_ROOT): - url = sickbeard.WEB_ROOT + url - - if self._headers_written: - raise Exception("Cannot redirect after headers have been written") - if status is None: - status = 301 if permanent else 302 - else: - assert isinstance(status, int) and 300 <= status <= 399 - self.set_status(status) - self.set_header("Location", urljoin(utf8(self.request.uri), - utf8(url))) - - def get_current_user(self): - if not isinstance(self, UI) and sickbeard.WEB_USERNAME and sickbeard.WEB_PASSWORD: - return self.get_secure_cookie('sickrage_user') - else: - return True - - -class WebHandler(BaseHandler): - def __init__(self, *args, **kwargs): - super(WebHandler, self).__init__(*args, **kwargs) - self.io_loop = IOLoop.current() - - executor = ThreadPoolExecutor(cpu_count()) - - @authenticated - @coroutine - def get(self, route, *args, **kwargs): - try: - # route -> method obj - route = route.strip('/').replace('.', '_') or 'index' - method = getattr(self, route) - - results = yield self.async_call(method) - self.finish(results) - - except Exception: - logger.log(u'Failed doing webui request "%s": %s' % (route, traceback.format_exc()), logger.DEBUG) - raise HTTPError(404) - - @run_on_executor - def async_call(self, function): - try: - kwargs = self.request.arguments - for arg, value in kwargs.iteritems(): - if len(value) == 1: - kwargs[arg] = value[0] - - result = function(**kwargs) - return result - except Exception: - logger.log(u'Failed doing webui callback: %s' % (traceback.format_exc()), logger.ERROR) - raise - - # post uses get method - post = get - - -class LoginHandler(BaseHandler): - def get(self, *args, **kwargs): - - if self.get_current_user(): - self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - else: - t = PageTemplate(rh=self, filename="login.mako") - self.finish(t.render(title="Login", header="Login", topmenu="login")) - - def post(self, *args, **kwargs): - - api_key = None - - username = sickbeard.WEB_USERNAME - password = sickbeard.WEB_PASSWORD - - if (self.get_argument('username') == username or not username) \ - and (self.get_argument('password') == password or not password): - api_key = sickbeard.API_KEY - - if sickbeard.NOTIFY_ON_LOGIN and not helpers.is_ip_private(self.request.remote_ip): - notifiers.notify_login(self.request.remote_ip) - - if api_key: - remember_me = int(self.get_argument('remember_me', default=0) or 0) - self.set_secure_cookie('sickrage_user', api_key, expires_days=30 if remember_me > 0 else None) - logger.log(u'User logged into the Medusa web interface', logger.INFO) - else: - logger.log(u'User attempted a failed login to the Medusa web interface from IP: ' + self.request.remote_ip, logger.WARNING) - - self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - -class LogoutHandler(BaseHandler): - def get(self, *args, **kwargs): - self.clear_cookie("sickrage_user") - self.redirect('/login/') - - -class KeyHandler(RequestHandler): - def __init__(self, *args, **kwargs): - super(KeyHandler, self).__init__(*args, **kwargs) - - def get(self, *args, **kwargs): - api_key = None - - try: - username = sickbeard.WEB_USERNAME - password = sickbeard.WEB_PASSWORD - - if (self.get_argument('u', None) == username or not username) and \ - (self.get_argument('p', None) == password or not password): - api_key = sickbeard.API_KEY - - self.finish({'success': api_key is not None, 'api_key': api_key}) - except Exception: - logger.log(u'Failed doing key request: %s' % (traceback.format_exc()), logger.ERROR) - self.finish({'success': False, 'error': 'Failed returning results'}) - - -@route('(.*)(/?)') -class WebRoot(WebHandler): - def __init__(self, *args, **kwargs): - super(WebRoot, self).__init__(*args, **kwargs) - - def index(self): - return self.redirect('/' + sickbeard.DEFAULT_PAGE + '/') - - def robots_txt(self): - """ Keep web crawlers out """ - self.set_header('Content-Type', 'text/plain') - return "User-agent: *\nDisallow: /" - - def apibuilder(self): - def titler(x): - return (helpers.remove_article(x), x)[not x or sickbeard.SORT_ARTICLE] - - main_db_con = db.DBConnection(row_type='dict') - shows = sorted(sickbeard.showList, lambda x, y: cmp(titler(x.name), titler(y.name))) - episodes = {} - - results = main_db_con.select( - 'SELECT episode, season, showid ' - 'FROM tv_episodes ' - 'ORDER BY season ASC, episode ASC' - ) - - for result in results: - if result['showid'] not in episodes: - episodes[result['showid']] = {} - - if result['season'] not in episodes[result['showid']]: - episodes[result['showid']][result['season']] = [] - - episodes[result['showid']][result['season']].append(result['episode']) - - if len(sickbeard.API_KEY) == 32: - apikey = sickbeard.API_KEY - else: - apikey = 'API Key not generated' - - t = PageTemplate(rh=self, filename='apiBuilder.mako') - return t.render(title='API Builder', header='API Builder', shows=shows, episodes=episodes, apikey=apikey, - commands=function_mapper) - - def showPoster(self, show=None, which=None): - media = None - media_format = ('normal', 'thumb')[which in ('banner_thumb', 'poster_thumb', 'small')] - - if which[0:6] == 'banner': - media = ShowBanner(show, media_format) - elif which[0:6] == 'fanart': - media = ShowFanArt(show, media_format) - elif which[0:6] == 'poster': - media = ShowPoster(show, media_format) - elif which[0:7] == 'network': - media = ShowNetworkLogo(show, media_format) - - if media is not None: - self.set_header('Content-Type', media.get_media_type()) - - return media.get_media() - - return None - - def setHomeLayout(self, layout): - - if layout not in ('poster', 'small', 'banner', 'simple', 'coverflow'): - layout = 'poster' - - sickbeard.HOME_LAYOUT = layout - # Don't redirect to default page so user can see new layout - return self.redirect("/home/") - - @staticmethod - def setPosterSortBy(sort): - - if sort not in ('name', 'date', 'network', 'progress'): - sort = 'name' - - sickbeard.POSTER_SORTBY = sort - sickbeard.save_config() - - @staticmethod - def setPosterSortDir(direction): - - sickbeard.POSTER_SORTDIR = int(direction) - sickbeard.save_config() - - def setHistoryLayout(self, layout): - - if layout not in ('compact', 'detailed'): - layout = 'detailed' - - sickbeard.HISTORY_LAYOUT = layout - - return self.redirect("/history/") - - def toggleDisplayShowSpecials(self, show): - - sickbeard.DISPLAY_SHOW_SPECIALS = not sickbeard.DISPLAY_SHOW_SPECIALS - - return self.redirect("/home/displayShow?show=" + show) - - def setScheduleLayout(self, layout): - if layout not in ('poster', 'banner', 'list', 'calendar'): - layout = 'banner' - - if layout == 'calendar': - sickbeard.COMING_EPS_SORT = 'date' - - sickbeard.COMING_EPS_LAYOUT = layout - - return self.redirect("/schedule/") - - def toggleScheduleDisplayPaused(self): - - sickbeard.COMING_EPS_DISPLAY_PAUSED = not sickbeard.COMING_EPS_DISPLAY_PAUSED - - return self.redirect("/schedule/") - - def setScheduleSort(self, sort): - if sort not in ('date', 'network', 'show'): - sort = 'date' - - if sickbeard.COMING_EPS_LAYOUT == 'calendar': - sort \ - = 'date' - - sickbeard.COMING_EPS_SORT = sort - - return self.redirect("/schedule/") - - def schedule(self, layout=None): - next_week = datetime.date.today() + datetime.timedelta(days=7) - next_week1 = datetime.datetime.combine(next_week, datetime.time(tzinfo=network_timezones.sb_timezone)) - results = ComingEpisodes.get_coming_episodes(ComingEpisodes.categories, sickbeard.COMING_EPS_SORT, False) - today = datetime.datetime.now().replace(tzinfo=network_timezones.sb_timezone) - - submenu = [ - { - 'title': 'Sort by:', - 'path': { - 'Date': 'setScheduleSort/?sort=date', - 'Show': 'setScheduleSort/?sort=show', - 'Network': 'setScheduleSort/?sort=network', - } - }, - { - 'title': 'Layout:', - 'path': { - 'Banner': 'setScheduleLayout/?layout=banner', - 'Poster': 'setScheduleLayout/?layout=poster', - 'List': 'setScheduleLayout/?layout=list', - 'Calendar': 'setScheduleLayout/?layout=calendar', - } - }, - { - 'title': 'View Paused:', - 'path': { - 'Hide': 'toggleScheduleDisplayPaused' - } if sickbeard.COMING_EPS_DISPLAY_PAUSED else { - 'Show': 'toggleScheduleDisplayPaused' - } - }, - ] - - # Allow local overriding of layout parameter - if layout and layout in ('poster', 'banner', 'list', 'calendar'): - layout = layout - else: - layout = sickbeard.COMING_EPS_LAYOUT - - t = PageTemplate(rh=self, filename='schedule.mako') - return t.render(submenu=submenu, next_week=next_week1, today=today, results=results, layout=layout, - title='Schedule', header='Schedule', topmenu='schedule', - controller="schedule", action="index") - - -class CalendarHandler(BaseHandler): - def get(self): - if sickbeard.CALENDAR_UNPROTECTED: - self.write(self.calendar()) - else: - self.calendar_auth() - - @authenticated - def calendar_auth(self): - self.write(self.calendar()) - - # Raw iCalendar implementation by Pedro Jose Pereira Vieito (@pvieito). - # - # iCalendar (iCal) - Standard RFC 5545To 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.