From 9cdf57f98975aaa363002639897b6352b1a165d0 Mon Sep 17 00:00:00 2001 From: Prinz23 Date: Sun, 23 Apr 2017 22:34:17 +0100 Subject: [PATCH 1/2] Add Trakt watchlist to Add show/Trakt Cards. Change revoke application access at Trakt when account is deleted in SG. Add support for 'DELETE' and 'PUT' requests in libtrakt. Fix use 'GET', 'POST' method in Trakt lib upgrade. --- .../interfaces/default/home_browseShows.tmpl | 12 ++ lib/libtrakt/trakt.py | 149 ++++++++++++------ sickbeard/webserve.py | 16 +- 3 files changed, 126 insertions(+), 51 deletions(-) diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl index a780c583..a11a2a73 100644 --- a/gui/slick/interfaces/default/home_browseShows.tmpl +++ b/gui/slick/interfaces/default/home_browseShows.tmpl @@ -196,6 +196,18 @@ $(document).ready(function(){ #else + #end if + + #if any($sg_var('TRAKT_ACCOUNTS', [])) + + #for $account in $sg_var('TRAKT_ACCOUNTS') + #if $sg_var('TRAKT_ACCOUNTS').get($account).active and $sg_var('TRAKT_ACCOUNTS').get($account).name + + #end if + #end for + #else + + #end if #elif 'IMDb' == $browse_type diff --git a/lib/libtrakt/trakt.py b/lib/libtrakt/trakt.py index 07dafa35..4096067d 100644 --- a/lib/libtrakt/trakt.py +++ b/lib/libtrakt/trakt.py @@ -15,26 +15,40 @@ class TraktAccount: def __init__(self, account_id=None, token='', refresh_token='', auth_fail=0, last_fail=None, token_valid_date=None): self.account_id = account_id self._name = '' + self._slug = '' self.token = token self.refresh_token = refresh_token self.auth_fail = auth_fail self.last_fail = last_fail self.token_valid_date = token_valid_date + def get_name_slug(self): + try: + resp = TraktAPI().trakt_request('users/settings', send_oauth=self.account_id, sleep_retry=20) + self.reset_auth_failure() + if 'user' in resp: + self._name = resp['user']['username'] + self._slug = resp['user']['ids']['slug'] + except TraktAuthException: + self.inc_auth_failure() + self._name = '' + except TraktException: + pass + + @property + def slug(self): + if self.token and self.active: + if not self._slug: + self.get_name_slug() + else: + self._slug = '' + return self._slug + @property def name(self): if self.token and self.active: if not self._name: - try: - resp = TraktAPI().trakt_request('users/settings', send_oauth=self.account_id, sleep_retry=20) - self.reset_auth_failure() - if 'user' in resp: - self._name = resp['user']['username'] - except TraktAuthException: - self.inc_auth_failure() - self._name = '' - except TraktException: - pass + self.get_name_slug() else: self._name = '' @@ -53,10 +67,10 @@ class TraktAccount: @property def token_expired(self): - return self.token_valid_date and datetime.datetime.now() > self.token_valid_date + return self.token_valid_date and self.token_valid_date < datetime.datetime.now() def reset_auth_failure(self): - if self.auth_fail != 0: + if 0 != self.auth_fail: self.auth_fail = 0 self.last_fail = None @@ -68,13 +82,13 @@ class TraktAccount: if self.auth_fail < self.max_auth_fail: if self.last_fail: time_diff = datetime.datetime.now() - self.last_fail - if self.auth_fail % 3 == 0: - if time_diff > datetime.timedelta(days=1): + if 0 == self.auth_fail % 3: + if datetime.timedelta(days=1) < time_diff: self.inc_auth_failure() sickbeard.save_config() - elif time_diff > datetime.timedelta(minutes=15): + elif datetime.timedelta(minutes=15) < time_diff: self.inc_auth_failure() - if self.auth_fail == self.max_auth_fail or time_diff > datetime.timedelta(hours=6): + if self.auth_fail == self.max_auth_fail or datetime.timedelta(hours=6) < time_diff: sickbeard.save_config() else: self.inc_auth_failure() @@ -99,19 +113,22 @@ class TraktAPI: return '!!!'.join('%s|%s|%s|%s|%s|%s' % ( value.account_id, value.token, value.refresh_token, value.auth_fail, value.last_fail.strftime('%Y%m%d%H%M') if value.last_fail else '0', - value.token_valid_date.strftime('%Y%m%d%H%M%S') if value.token_valid_date else '0') for (key, value) in data.items()) + value.token_valid_date.strftime('%Y%m%d%H%M%S') if value.token_valid_date else '0') + for (key, value) in data.items()) @staticmethod def read_config_string(data): return dict((int(a.split('|')[0]), TraktAccount( int(a.split('|')[0]), a.split('|')[1], a.split('|')[2], int(a.split('|')[3]), datetime.datetime.strptime(a.split('|')[4], '%Y%m%d%H%M') if a.split('|')[4] != '0' else None, - datetime.datetime.strptime(a.split('|')[5], '%Y%m%d%H%M%S') if a.split('|')[5] != '0' else None)) for a in data.split('!!!') if data) + datetime.datetime.strptime(a.split('|')[5], '%Y%m%d%H%M%S') if a.split('|')[5] != '0' else None)) + for a in data.split('!!!') if data) @staticmethod def add_account(token, refresh_token, token_valid_date): k = max(sickbeard.TRAKT_ACCOUNTS.keys() or [0]) + 1 - sickbeard.TRAKT_ACCOUNTS[k] = TraktAccount(account_id=k, token=token, refresh_token=refresh_token, token_valid_date=token_valid_date) + sickbeard.TRAKT_ACCOUNTS[k] = TraktAccount(account_id=k, token=token, refresh_token=refresh_token, + token_valid_date=token_valid_date) sickbeard.save_config() return k @@ -132,6 +149,7 @@ class TraktAPI: @staticmethod def delete_account(account): if account in sickbeard.TRAKT_ACCOUNTS: + TraktAPI().trakt_request('/oauth/revoke', send_oauth=account, method='POST') sickbeard.TRAKT_ACCOUNTS.pop(account) sickbeard.save_config() return True @@ -149,7 +167,7 @@ class TraktAPI: } if refresh: - if account and account in sickbeard.TRAKT_ACCOUNTS: + if None is not account and account in sickbeard.TRAKT_ACCOUNTS: data['grant_type'] = 'refresh_token' data['refresh_token'] = sickbeard.TRAKT_ACCOUNTS[account].refresh_token else: @@ -170,13 +188,22 @@ class TraktAPI: if 'access_token' in resp and 'refresh_token' in resp and 'expires_in' in resp: token_valid_date = now + datetime.timedelta(seconds=sickbeard.helpers.tryInt(resp['expires_in'])) - if refresh or (not refresh and account and account in sickbeard.TRAKT_ACCOUNTS): - return self.replace_account(account, resp['access_token'], resp['refresh_token'], token_valid_date, refresh) - else: - return self.add_account(resp['access_token'], resp['refresh_token'], token_valid_date) + if refresh or (not refresh and None is not account and account in sickbeard.TRAKT_ACCOUNTS): + return self.replace_account(account, resp['access_token'], resp['refresh_token'], + token_valid_date, refresh) + return self.add_account(resp['access_token'], resp['refresh_token'], token_valid_date) + return False - def trakt_request(self, path, data=None, headers=None, url=None, count=0, sleep_retry=60, send_oauth=None, **kwargs): + def trakt_request(self, path, data=None, headers=None, url=None, count=0, sleep_retry=60, + send_oauth=None, method=None, **kwargs): + + if method not in ['GET', 'POST', 'PUT', 'DELETE', None]: + return {} + if None is method: + method = ('GET', 'POST')['data' in kwargs.keys() or data is not None] + if path != 'oauth/token' and None is send_oauth and method in ['POST', 'PUT', 'DELETE']: + return {} count += 1 if count > self.max_retrys: @@ -186,7 +213,7 @@ class TraktAPI: count > 1 and time.sleep(sleep_retry) headers = headers or self.headers - if send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS: + if None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS: if sickbeard.TRAKT_ACCOUNTS[send_oauth].active: if sickbeard.TRAKT_ACCOUNTS[send_oauth].needs_refresh: self.trakt_token(refresh=True, count=0, account=send_oauth) @@ -202,54 +229,77 @@ class TraktAPI: url = url or self.api_url try: - resp = self.session.request(('GET', 'POST')['data' in kwargs.keys()], - url + path, **kwargs) + resp = self.session.request(method, '%s%s' % (url, path), **kwargs) + + if 'DELETE' == method: + result = None + if 204 == resp.status_code: + result = {'result': 'success'} + elif 404 == resp.status_code: + result = {'result': 'failed'} + if result and None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS: + sickbeard.TRAKT_ACCOUNTS[send_oauth].reset_auth_failure() + return result + resp.raise_for_status() + return {} # check for http errors and raise if any are present resp.raise_for_status() # convert response to json resp = resp.json() + except requests.RequestException as e: code = getattr(e.response, 'status_code', None) if not code: if 'timed out' in e: logger.log(u'Timeout connecting to Trakt', logger.WARNING) + return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, + send_oauth=send_oauth, method=method) # This is pretty much a fatal error if there is no status_code - # It means there basically was no response at all + # It means there basically was no response at all else: - logger.log(u'Could not connect to Trakt. Error: {0}'.format(e), logger.WARNING) + logger.log(u'Could not connect to Trakt. Error: {0}'.format(e), logger.WARNING) + elif 502 == code: # Retry the request, Cloudflare had a proxying issue logger.log(u'Retrying Trakt api request: %s' % path, logger.WARNING) - return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, send_oauth=send_oauth) + return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, + send_oauth=send_oauth, method=method) + elif 401 == code and path != 'oauth/token': - if send_oauth: + if None is not send_oauth: if sickbeard.TRAKT_ACCOUNTS[send_oauth].needs_refresh: if self.trakt_token(refresh=True, count=count, account=send_oauth): - return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, send_oauth=send_oauth) - else: - logger.log(u'Unauthorized. Please check your Trakt settings', logger.WARNING) - sickbeard.TRAKT_ACCOUNTS[send_oauth].auth_failure() - raise TraktAuthException() - else: - # sometimes the trakt server sends invalid token error even if it isn't + return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, + send_oauth=send_oauth, method=method) + + logger.log(u'Unauthorized. Please check your Trakt settings', logger.WARNING) sickbeard.TRAKT_ACCOUNTS[send_oauth].auth_failure() - if count >= self.max_retrys: - raise TraktAuthException() - else: - return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, send_oauth=send_oauth) - else: - raise TraktAuthException() + raise TraktAuthException() + + # sometimes the trakt server sends invalid token error even if it isn't + sickbeard.TRAKT_ACCOUNTS[send_oauth].auth_failure() + if count >= self.max_retrys: + raise TraktAuthException() + + return self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, + send_oauth=send_oauth, method=method) + + raise TraktAuthException() elif code in (500, 501, 503, 504, 520, 521, 522): # http://docs.trakt.apiary.io/#introduction/status-codes logger.log(u'Trakt may have some issues and it\'s unavailable. Trying again', logger.WARNING) - self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, send_oauth=send_oauth) + self.trakt_request(path, data, headers, url, count=count, sleep_retry=sleep_retry, + send_oauth=send_oauth, method=method) elif 404 == code: - logger.log(u'Trakt error (404) the resource does not exist: %s' % url + path, logger.WARNING) + logger.log(u'Trakt error (404) the resource does not exist: %s%s' % (url, path), logger.WARNING) else: logger.log(u'Could not connect to Trakt. Code error: {0}'.format(code), logger.ERROR) return {} + except ValueError as e: + logger.log(u'Value Error: {0}'.format(e), logger.ERROR) + return {} # check and confirm Trakt call did not fail if isinstance(resp, dict) and 'failure' == resp.get('status', None): @@ -257,9 +307,8 @@ class TraktAPI: raise TraktException(resp['message']) if 'error' in resp: raise TraktException(resp['error']) - else: - raise TraktException('Unknown Error') + raise TraktException('Unknown Error') - if send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS: + if None is not send_oauth and send_oauth in sickbeard.TRAKT_ACCOUNTS: sickbeard.TRAKT_ACCOUNTS[send_oauth].reset_auth_failure() return resp diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index c061740e..7baa3109 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -3321,6 +3321,20 @@ class NewHomeAddShows(Home): 'recommendations/shows?limit=%s&' % 100, 'Recommended for %s by Trakt' % name, mode='recommended-%s' % account, send_oauth=account) + def trakt_watchlist(self, *args, **kwargs): + + if 'add' == kwargs.get('action'): + return self.redirect('/config/notifications/#tabs-3') + + account = sickbeard.helpers.tryInt(kwargs.get('account'), None) + try: + name = sickbeard.TRAKT_ACCOUNTS[account].name + except KeyError: + return self.trakt_default() + return self.browse_trakt( + 'users/%s/watchlist/shows?limit=%s&' % (sickbeard.TRAKT_ACCOUNTS[account].slug, 100), 'WatchList for %s by Trakt' % name, + mode='watchlist-%s' % account, send_oauth=account) + def trakt_default(self): return self.redirect('/home/addShows/%s' % ('trakt_trending', sickbeard.TRAKT_MRU)[any(sickbeard.TRAKT_MRU)]) @@ -3330,7 +3344,7 @@ class NewHomeAddShows(Home): browse_type = 'Trakt' normalised, filtered = ([], []) - if not sickbeard.USE_TRAKT and 'recommended' in kwargs.get('mode', ''): + if not sickbeard.USE_TRAKT and ('recommended' in kwargs.get('mode', '') or 'watchlist' in kwargs.get('mode', '')): error_msg = 'To browse personal recommendations, enable Trakt.tv in Config/Notifications/Social' return self.browse_shows(browse_type, browse_title, filtered, error_msg=error_msg, show_header=1, **kwargs) From 9e7ad9f5b28511fe5d574807e163b718fd273f7f Mon Sep 17 00:00:00 2001 From: JackDandy Date: Sat, 20 May 2017 02:10:33 +0100 Subject: [PATCH 2/2] Add persistent hide/unhide cards to Add show/Trakt and Add show/IMDb Cards. Change simplify dropdowns at all Add show/Cards. --- CHANGES.md | 4 + gui/slick/css/dark.css | 4 + gui/slick/css/light.css | 4 + gui/slick/css/style.css | 6 + .../interfaces/default/home_browseShows.tmpl | 309 +++++++++++------- sickbeard/__init__.py | 7 +- sickbeard/webserve.py | 28 +- 7 files changed, 236 insertions(+), 126 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 05c9311d..04234984 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -52,6 +52,10 @@ * Change do not have shows checked by default on import page. To re-enable import shows checked by default, 1) On config page 'Save' 2) Stop SG 3) Find 'import_default_checked_shows' in config.ini and set '1' 4) Start SG * Add Nyaa (.si) torrent provider +* Add Trakt watchlist to Add show/Trakt Cards +* Change revoke application access at Trakt when account is deleted in SG +* Add persistent hide/unhide cards to Add show/Trakt and Add show/IMDb Cards +* Change simplify dropdowns at all Add show/Cards [develop changelog] diff --git a/gui/slick/css/dark.css b/gui/slick/css/dark.css index c651622d..fe1bb8a5 100644 --- a/gui/slick/css/dark.css +++ b/gui/slick/css/dark.css @@ -293,11 +293,13 @@ a.ui-font{ background-image:linear-gradient(to left, rgba(51, 51, 51, 1), rgba(51, 51, 51, 0)) } +.show-toggle-hide, td.tvShow a{ color:#ddd; text-decoration:none } +.show-toggle-hide:hover, td.tvShow a:hover span, td.tvShow a:hover{ cursor:pointer; @@ -1288,6 +1290,7 @@ input sizing (for config pages) .showlist-select optgroup, #pickShow optgroup, #showfilter optgroup, +#showsort optgroup, #editAProvider optgroup{ color:#eee; background-color:rgb(51, 51, 51) @@ -1296,6 +1299,7 @@ input sizing (for config pages) .showlist-select optgroup option, #pickShow optgroup option, #showfilter optgroup option, +#showsort optgroup option, #editAProvider optgroup option{ color:#222; background-color:#ddd diff --git a/gui/slick/css/light.css b/gui/slick/css/light.css index b47c0e41..d031ef28 100644 --- a/gui/slick/css/light.css +++ b/gui/slick/css/light.css @@ -308,11 +308,13 @@ a.ui-font{ background-image:linear-gradient(to left, rgba(223, 218, 207, 1), rgba(223, 218, 207, 0)) } +.show-toggle-hide, td.tvShow a{ color:#000; text-decoration:none } +.show-toggle-hide:hover, td.tvShow a:hover span, td.tvShow a:hover{ cursor:pointer; @@ -1254,6 +1256,7 @@ input sizing (for config pages) .showlist-select optgroup, #pickShow optgroup, #showfilter optgroup, +#showsort optgroup, #editAProvider optgroup{ color:#eee; background-color:#888 @@ -1262,6 +1265,7 @@ input sizing (for config pages) .showlist-select optgroup option, #pickShow optgroup option, #showfilter optgroup option, +#showsort optgroup option, #editAProvider optgroup option{ color:#222; background-color:#fff diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css index ff26b39d..78eca8f2 100644 --- a/gui/slick/css/style.css +++ b/gui/slick/css/style.css @@ -882,6 +882,12 @@ home.tmpl background-image:linear-gradient(to left, rgba(223, 218, 207, 1), rgba(223, 218, 207, 0)) } +.show-toggle-hide{ + position:absolute; + top:272px; + right:2px +} + .show-date{ position:relative; overflow:hidden; diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl index a11a2a73..d4f7a12a 100644 --- a/gui/slick/interfaces/default/home_browseShows.tmpl +++ b/gui/slick/interfaces/default/home_browseShows.tmpl @@ -38,41 +38,40 @@ $(document).ready(function(){ // initialise combos for dirty page refreshes - $('#showsort').val('original'); - $('#showsortdirection').val('asc'); - $('#showfilter').val('*'); + $('#showsort').val('*'); - var $container = [$('#container')]; - jQuery.each($container, function(j){ - this.isotope({ - itemSelector: '.show-card', - sortBy: 'original-order', - layoutMode: 'masonry', - masonry: { - columnWidth: 188, - isFitWidth: !0, - gutter: 12 - }, - getSortData: { - premiered: '[data-premiered] parseInt', - name: function( itemElem ) { - var name = $( itemElem ).attr('data-name') || ''; + $('#container').isotope({ + itemSelector: '.show-card', + sortBy: 'original-order', + layoutMode: 'masonry', + masonry: { + columnWidth: 188, + isFitWidth: !0, + gutter: 12 + }, + getSortData: { + premiered: '[data-premiered] parseInt', + name: function( itemElem ) { + var name = $( itemElem ).attr('data-name') || ''; #end raw -#if not $sg_var('SORT_ARTICLE'): - name = name.replace(/^(?:(?:A(?!\s+to)n?)|The)\s(\w)/i, '$1'); +#if not $sg_var('SORT_ARTICLE') + name = name.replace(/^(?:(?:A(?!\s+to)n?)|The)\s(\w)/i, '$1'); #end if #raw - return name.toLowerCase(); - }, - rating: '[data-rating] parseInt', - votes: '[data-votes] parseInt', - } - }); + return name.toLowerCase(); + }, + rating: '[data-rating] parseInt', + votes: '[data-votes] parseInt', + } }); $('#showsort').on('change', function(){ var sortCriteria, el$ = $('#container'), shuffle = !1; - switch (this.value) { + switch (this.value.replace('by_', '')) { + case 'asc': + case 'desc': + sortCriteria = 'order'; + break; case 'original': sortCriteria = 'original-order' break; @@ -95,33 +94,84 @@ $(document).ready(function(){ break; } - if (!shuffle){ + var showSort = $('#showsort option'); + if('order' === sortCriteria){ + showSort.filter($('option[value="asc"], option[value="desc"]')).removeClass('selected'); + showSort.filter($('option[value="' + this.value + '"]')).addClass('selected'); + el$.one('layoutComplete', llUpdate); - el$.isotope({sortBy: sortCriteria}); - } else { - // shuffle rating_votes where it can already be sorted by ratings which leaves it nothing to do. - function t(sortCriteria, lastPhase){ - return function(){ - var el$ = $('#container'); - if (!lastPhase){ - lastPhase = !0; - el$.isotope({sortBy: sortCriteria}); - } else { - el$.off('layoutComplete'); - el$.isotope('layout'); - llUpdate(); + el$.isotope({sortAscending: 'asc' == this.value}); + } else if(0 == this.value.indexOf('by_')){ + showSort.filter($('option[value^="by_"][class*="selected"]')).removeClass('selected'); + showSort.filter($('option[value="' + this.value + '"]')).addClass('selected'); + + if (!shuffle){ + el$.one('layoutComplete', llUpdate); + el$.isotope({sortBy: sortCriteria}); + } else { + // shuffle rating_votes where it can already be sorted by ratings which leaves it nothing to do. + function t(sortCriteria, lastPhase){ + return function(){ + var el$ = $('#container'); + if (!lastPhase){ + lastPhase = !0; + el$.isotope({sortBy: sortCriteria}); + } else { + el$.off('layoutComplete'); + el$.isotope('layout'); + llUpdate(); + } } } + el$.on('layoutComplete', t(sortCriteria, !1)); + el$.isotope({sortBy: 'random'}); } - el$.on('layoutComplete', t(sortCriteria, !1)); - el$.isotope({sortBy: 'random'}); + } else { + showSort.filter($('option[value^="*"], #showsort option[value^="."]')).removeClass('selected'); + showSort.filter($('option[value="' + this.value + '"]')).addClass('selected'); + + var showCards = $('.show-card'), filter = this.value; + if('.hide' === this.value){ + showCards.filter($('.hide')).removeClass('hide').addClass('to-hide'); + filter = '.to-hide'; + } else { + showCards.filter($('.to-hide')).removeClass('to-hide').addClass('hide'); + } + + var el$ = $('#container'); + el$.one('layoutComplete', llUpdate); + el$.isotope({ filter: filter }); } }); - $('#showsortdirection').on('change', function(){ - var el$ = $('#container') - el$.one('layoutComplete', llUpdate); - el$.isotope({sortAscending: ('asc' == this.value)}); + $('#container').on('click', '.show-toggle-hide', function(event){ + var that = $(this); + event.preventDefault(); + + $.getJSON(this.href, function(data){ + if(data.success){ + var showCards = $('.show-card'), thisCard = $(that).parents('div[class*="show-card "]'), + numShows = showCards.length, numHidden, showSort = $('#showsort option'), filter = 'to-hide'; + + if(thisCard.hasClass(filter)){ + title = 'Hide'; + thisCard.removeClass(filter); + } else { + filter = 'hide'; + title = 'Unhide'; + thisCard.addClass(filter); + } + numHidden = showCards.filter($('.' + filter)).length; + that.attr('title', title); + + showSort.filter($('option[value=".hide"]')).text('Hidden (' + numHidden + ')'); + showSort.filter($('option[value="*"]')).text('All (' + (0 == numHidden ? '' : (numShows - numHidden) + '/') + numShows + ')'); + + var el$ = $('#container') + el$.on('layoutComplete', llUpdate); + el$.isotope(); + } + }); }); $('#showfilter').on('change', function(){ @@ -160,15 +210,35 @@ $(document).ready(function(){ #set $mode = $kwargs and $kwargs.get('mode', '') #if $all_shows or ($kwargs and $kwargs.get('show_header'))
- View: - + #set $num_all = len($all_shows) #set $selected = ' class="selected"' - #if 'Trakt' == $browse_type + + + + + #if 'Ani' not in $browse_type + + #end if + + + + + + + + + + + + + + + + #if 'Ani' not in $browse_type + #end if - - - Sort By: - - - Sort Order: -

$browse_title

- #if $kwargs and $kwargs.get('oldest'): + #if $kwargs and $kwargs.get('oldest')
First aired from $kwargs['oldest'] until $kwargs['newest']
@@ -264,7 +319,7 @@ $(document).ready(function(){
#if $all_shows #set $poster_id = 0 - #for $this_show in $all_shows: + #for $this_show in $all_shows #set $poster_id += 1 #set $title_html = $this_show['title'].replace('"', '"').replace("'", ''') @@ -276,7 +331,15 @@ $(document).ready(function(){ #set $overview = $this_show['overview'] #end if -
+ #set $known = 'not' + #set $show_id = $this_show['show_id'] + #if ':' in $show_id + #set $known = '' + #set $show_id = $show_id[2:] + #end if + #set $hide = ('', 'hide ')[$show_id in $sickbeard.BROWSELIST_HIDDEN] + +
#if $kwargs and 'newseasons' == $mode#Air#else#First air#end if##echo ('s', 'ed')[$this_show['when_past']]#: $this_show['premiered_str'] #if $this_show.get('ended_str')# - Ended: $this_show['ended_str']#end if#

Click for more at $browse_type"> - #if 'poster' in $this_show['images']: + #if 'poster' in $this_show['images'] #set $image = $this_show['images']['poster']['thumb'] - #else: + #else   #end if
@@ -298,17 +361,19 @@ $(document).ready(function(){
#echo ((re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\1 \2', $this_show['title']), $this_show['title'])[$sg_var('SORT_ARTICLE')], ' ')['' == $this_show['title']]#
- + #if 'Ani' not in $browse_type + + #end if

$this_show['rating']%$this_show['votes'] votes

- #if 'url_tvdb' in $this_show and $this_show['url_tvdb']: + #if 'url_tvdb' in $this_show and $this_show['url_tvdb'] tvdb #end if
- #if ':' in $this_show['show_id']: + #if ':' in $this_show['show_id']

In library

#else Add Show @@ -323,7 +388,7 @@ $(document).ready(function(){
#end for
- #if $kwargs and $kwargs.get('footnote'): + #if $kwargs and $kwargs.get('footnote')
$kwargs['footnote']
@@ -331,7 +396,7 @@ $(document).ready(function(){ #else

- #if $kwargs and $kwargs.get('error_msg'): + #if $kwargs and $kwargs.get('error_msg') $kwargs['error_msg'] #else $browse_type API did not return results, this can happen from time to time. diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index 4afcd539..fda97e5a 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -450,6 +450,7 @@ EPISODE_VIEW_DISPLAY_PAUSED = False EPISODE_VIEW_POSTERS = True EPISODE_VIEW_MISSED_RANGE = None HISTORY_LAYOUT = None +BROWSELIST_HIDDEN = [] FUZZY_DATING = False TRIM_ZERO = False @@ -541,7 +542,7 @@ def initialize(console_logging=True): EPISODE_VIEW_MISSED_RANGE, EPISODE_VIEW_POSTERS, FANART_PANEL, FANART_RATINGS, \ EPISODE_VIEW_VIEWMODE, EPISODE_VIEW_BACKGROUND, EPISODE_VIEW_BACKGROUND_TRANSLUCENT, \ DISPLAY_SHOW_VIEWMODE, DISPLAY_SHOW_BACKGROUND, DISPLAY_SHOW_BACKGROUND_TRANSLUCENT, \ - DISPLAY_SHOW_VIEWART, DISPLAY_SHOW_MINIMUM, DISPLAY_SHOW_SPECIALS, HISTORY_LAYOUT + DISPLAY_SHOW_VIEWART, DISPLAY_SHOW_MINIMUM, DISPLAY_SHOW_SPECIALS, HISTORY_LAYOUT, BROWSELIST_HIDDEN # Gen Config/Misc global LAUNCH_BROWSER, UPDATE_SHOWS_ON_START, SHOW_UPDATE_HOUR, \ TRASH_REMOVE_SHOW, TRASH_ROTATE_LOGS, ACTUAL_LOG_DIR, LOG_DIR, INDEXER_TIMEOUT, ROOT_DIRS, \ @@ -1099,6 +1100,9 @@ def initialize(console_logging=True): EPISODE_VIEW_MISSED_RANGE = check_setting_int(CFG, 'GUI', 'episode_view_missed_range', 7) HISTORY_LAYOUT = check_setting_str(CFG, 'GUI', 'history_layout', 'detailed') + BROWSELIST_HIDDEN = [ + x.strip() for x in check_setting_str(CFG, 'GUI', 'browselist_hidden', '').split('|~|') if x.strip()] + # initialize NZB and TORRENT providers providerList = providers.makeProviderList() @@ -1849,6 +1853,7 @@ def save_config(): new_config['GUI']['showlist_tagview'] = SHOWLIST_TAGVIEW new_config['GUI']['show_tag_default'] = SHOW_TAG_DEFAULT new_config['GUI']['history_layout'] = HISTORY_LAYOUT + new_config['GUI']['browselist_hidden'] = '|~|'.join(BROWSELIST_HIDDEN) new_config['Subtitles'] = {} new_config['Subtitles']['use_subtitles'] = int(USE_SUBTITLES) diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 7baa3109..be8414bc 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -3373,6 +3373,10 @@ class NewHomeAddShows(Home): except (IndexError, KeyError): pass + if not normalised: + error_msg = 'No items in watchlist. Use the "Add to watchlist" button at the Trakt website' + return self.browse_shows(browse_type, browse_title, filtered, error_msg=error_msg, show_header=1, **kwargs) + oldest_dt = 9999999 newest_dt = 0 oldest = None @@ -3427,7 +3431,7 @@ class NewHomeAddShows(Home): kwargs.update(dict(oldest=oldest, newest=newest, error_msg=error_msg)) - if 'recommended' not in kwargs.get('mode', ''): + if 'recommended' not in kwargs.get('mode', '') and 'watchlist' not in kwargs.get('mode', ''): mode = kwargs.get('mode', '').split('-') if mode: func = 'trakt_%s' % mode[0] @@ -3438,6 +3442,20 @@ class NewHomeAddShows(Home): sickbeard.save_config() return self.browse_shows(browse_type, browse_title, filtered, **kwargs) + @staticmethod + def show_toggle_hide(ids): + save_config = False + for sid in ids.split(':'): + if 3 < len(sid) < 12: + save_config = True + if sid in sickbeard.BROWSELIST_HIDDEN: + sickbeard.BROWSELIST_HIDDEN.remove(sid) + else: + sickbeard.BROWSELIST_HIDDEN += [sid] + if save_config: + sickbeard.save_config() + return json.dumps({'success': save_config}) + @staticmethod def encode_html(text): @@ -3463,7 +3481,8 @@ class NewHomeAddShows(Home): t.kwargs = kwargs dedupe = [] - t.all_shows_inlibrary = 0 + t.num_inlibrary = 0 + t.num_hidden = 0 for item in shows: item['show_id'] = '' for index, tvdb in enumerate(['tvdb', 'tvrage']): @@ -3475,7 +3494,7 @@ class NewHomeAddShows(Home): # check tvshow indexer is not using the same id from another indexer if tvshow and (index + 1) == tvshow.indexer: item['show_id'] = u'%s:%s' % (tvshow.indexer, tvshow.indexerid) - t.all_shows_inlibrary += 1 + t.num_inlibrary += 1 break if None is not config.to_int(item['show_id'], None): @@ -3488,6 +3507,9 @@ class NewHomeAddShows(Home): dedupe.append(item['show_id']) t.all_shows.append(item) + if item['show_id'].split(':')[-1] in sickbeard.BROWSELIST_HIDDEN: + t.num_hidden += 1 + return t.respond() def import_shows(self, *args, **kwargs):