diff --git a/CHANGES.md b/CHANGES.md
index 7c52b6f..78766eb 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,6 +1,7 @@
### 0.22.0 (2020-xx-xx xx:xx:xx UTC)
* Add menu Shows/"Metacritic Cards"
+* Add menu Shows/"TV Calendar Cards"
* Change make web UI calls async so that, for example, process media will not block page requests
* Change improve speed of backlog overview
* Fix the missing snatched low quality on backlog overview
diff --git a/gui/slick/css/fonts/sgicons.eot b/gui/slick/css/fonts/sgicons.eot
index 0333021..7253f34 100644
Binary files a/gui/slick/css/fonts/sgicons.eot and b/gui/slick/css/fonts/sgicons.eot differ
diff --git a/gui/slick/css/fonts/sgicons.svg b/gui/slick/css/fonts/sgicons.svg
index 93b5191..537201e 100644
--- a/gui/slick/css/fonts/sgicons.svg
+++ b/gui/slick/css/fonts/sgicons.svg
@@ -106,6 +106,7 @@
+
diff --git a/gui/slick/css/fonts/sgicons.ttf b/gui/slick/css/fonts/sgicons.ttf
index c96a7b5..dea7a1a 100644
Binary files a/gui/slick/css/fonts/sgicons.ttf and b/gui/slick/css/fonts/sgicons.ttf differ
diff --git a/gui/slick/css/fonts/sgicons.woff b/gui/slick/css/fonts/sgicons.woff
index 9da9f81..a5efba7 100644
Binary files a/gui/slick/css/fonts/sgicons.woff and b/gui/slick/css/fonts/sgicons.woff differ
diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css
index cb24f57..33f0b89 100644
--- a/gui/slick/css/style.css
+++ b/gui/slick/css/style.css
@@ -678,6 +678,10 @@ inc_top.tmpl
content:"\e890"
}
+.sgicon-tvc:before{
+ content:"\e891"
+}
+
.sgicon-imdb:before{
content:"\e898"
}
@@ -1415,6 +1419,25 @@ home_browseShows.tmpl
background-image:url("../images/poster-dark.jpg")
}
+#browse-list.tvcalendar .browse-image{
+ height:104px /* 62% of image height */
+}
+
+#browse-list.tvcalendar .show-toggle-hide{
+ top:105px
+}
+
+#browse-list.tvcalendar .show-card{
+ height:172px /* -169px */
+}
+
+#browse-list.tvcalendar.no-votes .show-card{
+ height:155px
+}
+
+#browse-list.tvcalendar .show-card-inner .heart.icon-glyph{
+ margin-left:0
+}
/* =======================================================================
home_postprocess.tmpl
========================================================================== */
diff --git a/gui/slick/interfaces/default/home_browseShows.tmpl b/gui/slick/interfaces/default/home_browseShows.tmpl
index 9efb227..c03eec3 100644
--- a/gui/slick/interfaces/default/home_browseShows.tmpl
+++ b/gui/slick/interfaces/default/home_browseShows.tmpl
@@ -7,11 +7,15 @@
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
+#set $mode = $kwargs and $kwargs.get('mode', '')
+#set $use_votes = $kwargs and $kwargs.get('use_votes', True)
+#set $use_ratings = $kwargs and $kwargs.get('use_ratings', True)
+##
#set global $title='Browse %s Shows' % $browse_type
#set global $header='Browse Shows'
#set global $sbPath='..'
#set global $topmenu='home'
-#set global $page_body_attr = 'browse-list'
+#set global $page_body_attr = 'browse-list" class="%s%s' % ($browse_type.lower(), ('', ' no-votes')[not $use_votes])
#set sg_root = $getVar('sbRoot', WEB_ROOT)
##
#import os.path
@@ -174,7 +178,7 @@ $(document).ready(function(){
$('#showfilter').on('change', function(){
var filterValue = this.value;
- if (-1 == filterValue.indexOf('trakt') && -1 == filterValue.indexOf('imdb') && -1 == filterValue.indexOf('mc_') && -1 == filterValue.indexOf('default')) {
+ if (-1 == filterValue.indexOf('trakt') && -1 == filterValue.indexOf('imdb') && -1 == filterValue.indexOf('mc_') && -1 == filterValue.indexOf('tvc_') && -1 == filterValue.indexOf('default')) {
var el$ = $('#container')
el$.on('layoutComplete', llUpdate);
el$.isotope({ filter: filterValue });
@@ -208,8 +212,6 @@ $(document).ready(function(){
#end if
%s' % $heading#
-#set $mode = $kwargs and $kwargs.get('mode', '')
-#set $use_votes = $kwargs and $kwargs.get('use_votes')
#if $all_shows or ($kwargs and $kwargs.get('show_header'))
@@ -234,8 +236,10 @@ $(document).ready(function(){
#if $use_votes
Votes
#end if
+ #if $use_ratings
% Rating
- #if $use_votes
+ #end if
+ #if $use_ratings and $use_votes
% Rating > Votes
#end if
@@ -322,6 +326,13 @@ $(document).ready(function(){
... list more
#end if
+ #elif 'TVCalendar' == $browse_type
+
+ #for $page in $kwargs.get('pages') or []
+ $page[1]
+ #end for
+ Latest additions
+
#end if
#end if
@@ -352,15 +363,18 @@ $(document).ready(function(){
#set $show_id = $this_show.get('show_id')
#set $known = ('not', '')[bool($this_show.get('indb'))]
#set $hide = ('', 'hide ')[bool($this_show.get('hide'))]
+ #set $data_rating = re.search(r'^\d+$', '%s' % $this_show['rating']) and $this_show['rating'] or 0
-
+
#if 'poster' in $this_show['images']
#set $image = $this_show['images']['poster']['thumb']
@@ -379,7 +393,9 @@ $(document).ready(function(){
#end if
-
$this_show['rating']% #if $use_votes#$this_show['votes'] votes #end if#
+ #if $use_ratings or $use_votes
+
#if $use_ratings#$this_show['rating']%#end if##if $use_votes#$this_show['votes'] votes #end if#
#slurp#
+ #end if
#if 'url_tvdb' in $this_show and $this_show['url_tvdb']
@@ -413,7 +429,7 @@ $(document).ready(function(){
#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.
+ $browse_type did not return results, this can happen from time to time.
This view should auto refresh every 10 mins.
#end if
diff --git a/gui/slick/interfaces/default/inc_top.tmpl b/gui/slick/interfaces/default/inc_top.tmpl
index 3190c39..352d597 100644
--- a/gui/slick/interfaces/default/inc_top.tmpl
+++ b/gui/slick/interfaces/default/inc_top.tmpl
@@ -173,6 +173,13 @@
#end if
Metacritic Cards
+#set $tvc_mode = 'new shows'
+#set $tvc_modes = dict(tvc_newshows='new shows', tvc_returning='returning', tvc_latest='latest')
+#if $sg_var('TVC_MRU') in $tvc_modes
+ #set $tvc_mode = $tvc_modes[$sg_var('TVC_MRU')]
+#end if
+ TV Calendar Cards
+
#if $sg_var('USE_ANIDB')
Anime Cards
@@ -182,7 +189,7 @@
#end if
-#set $added_last = ($sg_var('showList', []) or [])[-7:][::-1]
+#set $added_last = ($sg_var('showList', []) or [])[-9:][::-1]
#if not $added_last:
This will list added shows
#else
diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py
index 81478a2..0379d75 100755
--- a/sickbeard/__init__.py
+++ b/sickbeard/__init__.py
@@ -584,6 +584,7 @@ else:
TRAKT_BASE_URL = 'https://api.trakt.tv/'
MC_MRU = ''
+TVC_MRU = ''
COOKIE_SECRET = b64encodestring(uuid.uuid4().bytes + uuid.uuid4().bytes)
@@ -721,7 +722,7 @@ def init_stage_1(console_logging):
global USE_TRAKT, TRAKT_CONNECTED_ACCOUNT, TRAKT_ACCOUNTS, TRAKT_MRU, TRAKT_VERIFY, \
TRAKT_USE_WATCHLIST, TRAKT_REMOVE_WATCHLIST, TRAKT_TIMEOUT, TRAKT_METHOD_ADD, TRAKT_START_PAUSED, \
TRAKT_SYNC, TRAKT_DEFAULT_INDEXER, TRAKT_REMOVE_SERIESLIST, TRAKT_UPDATE_COLLECTION, \
- MC_MRU, \
+ MC_MRU, TVC_MRU, \
USE_SLACK, SLACK_NOTIFY_ONSNATCH, SLACK_NOTIFY_ONDOWNLOAD, SLACK_NOTIFY_ONSUBTITLEDOWNLOAD, \
SLACK_CHANNEL, SLACK_AS_AUTHED, SLACK_BOT_NAME, SLACK_ICON_URL, SLACK_ACCESS_TOKEN, \
USE_DISCORDAPP, DISCORDAPP_NOTIFY_ONSNATCH, DISCORDAPP_NOTIFY_ONDOWNLOAD, \
@@ -1139,6 +1140,7 @@ def init_stage_1(console_logging):
TRAKT_MRU = check_setting_str(CFG, 'Trakt', 'trakt_mru', '')
MC_MRU = check_setting_str(CFG, 'Metacritic', 'mc_mru', '')
+ TVC_MRU = check_setting_str(CFG, 'TVCalendar', 'tvc_mru', '')
USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0))
PYTIVO_HOST = check_setting_str(CFG, 'pyTivo', 'pytivo_host', '')
@@ -2055,6 +2057,9 @@ def save_config():
('Metacritic', [
('mru', MC_MRU)
]),
+ ('TVCalendar', [
+ ('mru', TVC_MRU)
+ ]),
('Slack', [
('use_%s', int(USE_SLACK)),
('channel', SLACK_CHANNEL),
diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py
index 283b0b7..58186c1 100644
--- a/sickbeard/webserve.py
+++ b/sickbeard/webserve.py
@@ -220,7 +220,7 @@ class RouteHandler(LegacyBaseHandler):
method_args += [item for item in arg if None is not item]
if 'kwargs' in method_args or re.search('[A-Z]', route):
# no filtering for legacy and routes that depend on *args and **kwargs
- result = yield self.async_call(method, request_kwargs) # method(**request_kwargs)
+ result = yield self.async_call(method, request_kwargs) # method(**request_kwargs)
else:
filter_kwargs = dict(filter_iter(lambda kv: kv[0] in method_args, iteritems(request_kwargs)))
result = yield self.async_call(method, filter_kwargs) # method(**filter_kwargs)
@@ -810,8 +810,8 @@ class LoadingWebHandler(BaseHandler):
@authenticated
@gen.coroutine
def get(self, route, *args, **kwargs):
- yield self.route_method(route, use_404=True, limit_route=(lambda _route: not re.search('get[_-]message', _route)
- and 'loading-page' or _route))
+ yield self.route_method(route, use_404=True, limit_route=(
+ lambda _route: not re.search('get[_-]message', _route) and 'loading-page' or _route))
post = get
@@ -4411,6 +4411,220 @@ class AddShows(Home):
if not filter_list(lambda tvid_prodid: helpers.find_show_by_id(tvid_prodid), ids.split(' ')):
return self.new_show('|'.join(['', '', '', ids or show_name]), use_show_name=True)
+ def tvc_default(self):
+
+ return self.redirect('/add-shows/%s' % ('tvc_newshows', sickbeard.TVC_MRU)[any(sickbeard.TVC_MRU)])
+
+ def tvc_newshows(self, **kwargs):
+ return self.browse_tvc(
+ 'TV-shows-starting-', 'New Shows at TV Calendar', mode='newshows', **kwargs)
+
+ def tvc_returning(self, **kwargs):
+ return self.browse_tvc(
+ 'TV-shows-starting-', 'Returning Shows at TV Calendar', mode='returning', **kwargs)
+
+ def tvc_latest(self, **kwargs):
+ return self.browse_tvc(
+ 'recent-additions', 'Latest new shows at TV Calendar', mode='latest', **kwargs)
+
+ def browse_tvc(self, url_path, browse_title, **kwargs):
+
+ browse_type = 'TVCalendar'
+
+ footnote = None
+ filtered = []
+ today = datetime.datetime.today()
+ months = ['', 'January', 'February', 'March', 'April', 'May', 'June', 'July',
+ 'August', 'September', 'October', 'November', 'December']
+ this_month = '%s-%s' % (months[today.month], today.strftime('%Y'))
+ section = ''
+ if kwargs.get('mode', '') in ('newshows', 'returning'):
+ section = (kwargs.get('page') or this_month)
+ url_path += section
+
+ import browser_ua
+ url = 'https://www.pogdesign.co.uk/cat/%s' % url_path
+ html = helpers.get_url(url, headers={'User-Agent': browser_ua.get_ua()})
+ use_votes = False
+ if html:
+ prev_month = dt_prev_month = None
+ if kwargs.get('mode', '') in ('newshows', 'returning'):
+ try:
+ prev_month = re.findall('(?i)href="/cat/tv-shows-starting-([^"]+)', html)[0]
+ dt_prev_month = dateutil.parser.parse('1-%s' % prev_month)
+ except (BaseException, Exception):
+ prev_month = None
+ get_prev_month = (lambda _dt: _dt.replace(day=1) - datetime.timedelta(days=1))
+ get_next_month = (lambda _dt: _dt.replace(day=28) + datetime.timedelta(days=5))
+ get_month_year = (lambda _dt: '%s-%s' % (months[_dt.month], _dt.strftime('%Y')))
+ if prev_month:
+ dt_next_month = get_next_month(dt_prev_month)
+ while True:
+ next_month = get_month_year(dt_next_month)
+ if next_month not in (this_month, kwargs.get('page')):
+ break
+ dt_next_month = get_next_month(dt_next_month)
+ while True:
+ if prev_month not in (this_month, kwargs.get('page')):
+ break
+ dt_prev_month = get_prev_month(dt_prev_month)
+ prev_month = get_month_year(dt_prev_month)
+
+ else:
+ dt_next_month = get_next_month(today)
+ next_month = get_month_year(dt_next_month)
+ dt_prev_month = get_prev_month(today)
+ prev_month = get_month_year(dt_prev_month)
+
+ suppress_item = not kwargs.get('page') or kwargs.get('page') == this_month
+ get_date_text = (lambda m_y: m_y.replace('-', ' '))
+ next_month_text = get_date_text(next_month)
+ prev_month_text = get_date_text(prev_month)
+ page_month_text = suppress_item and 'void' or get_date_text(kwargs.get('page'))
+ kwargs.update(dict(pages=[
+ ('tvc_newshows?page=%s' % this_month, 'New this month'),
+ ('tvc_newshows?page=%s' % next_month, '...in %s' % next_month_text)] +
+ ([('tvc_newshows?page=%s' % kwargs.get('page'), '...in %s' % page_month_text)], [])[suppress_item] +
+ [('tvc_newshows?page=%s' % prev_month, '...in %s' % prev_month_text)] +
+ [('tvc_returning?page=%s' % this_month, 'Returning this month'),
+ ('tvc_returning?page=%s' % next_month, '...in %s' % next_month_text)] +
+ ([('tvc_returning?page=%s' % kwargs.get('page'), '...in %s' % page_month_text)], [])[suppress_item] +
+ [('tvc_returning?page=%s' % prev_month, '...in %s' % prev_month_text)]
+ ))
+
+ with BS4Parser(html, parse_only=dict(div={'class': (lambda at: at and 'pgwidth' in at)})) as tbl:
+ shows = []
+ if kwargs.get('mode', '') in ('latest', 'newshows', 'returning'):
+ tags = tbl.select('h2[class*="midtitle"], div[class*="contbox"]')
+ collect = False
+ for cur_tag in tags:
+ if re.match(r'(?i)h\d+', cur_tag.name) and 'midtitle' in cur_tag.attrs.get('class', []):
+ text = cur_tag.get_text(strip=True)
+ if kwargs['mode'] in ('latest', 'newshows'):
+ if not collect and ('Latest' in text or 'New' in text):
+ collect = True
+ continue
+ break
+ elif 'New' in text:
+ continue
+ elif 'Return' in text:
+ collect = True
+ continue
+ break
+ if collect:
+ shows += [cur_tag]
+
+ if not len(shows):
+ kwargs['error_msg'] = 'No TV titles found in %s %s,' \
+ ' try another selection' % (
+ helpers.anon_url(url), browse_title, (' for %s' % section.replace('-', ' '), '')[not section])
+
+ # build batches to correct htmlentity typos in overview
+ from html5lib.constants import entities
+ batches = []
+ for cur_n, cur_name in enumerate(entities):
+ if 0 == cur_n % 150:
+ if cur_n:
+ batches += [batch]
+ batch = []
+ batch += [cur_name]
+ else:
+ batches += [batch]
+
+ oldest, newest, oldest_dt, newest_dt = None, None, 9999999, 0
+ for row in shows:
+ try:
+ ids = dict(custom=row.select('input[type="checkbox"]')[0].attrs['value'], name='tvc')
+ info = row.find('a', href=re.compile('^/cat'))
+ url_path = info['href'].strip()
+
+ title = info.find('h2').get_text(strip=True)
+ img_uri = info.get('data-original', '').strip()
+ if not img_uri:
+ img_uri = re.findall(r'(?i).*?image:\s*url\(([^)]+)', info.attrs['style'])[0].strip()
+ images = dict(poster=dict(thumb='imagecache?path=browse/thumb/tvc&source=%s' % img_uri))
+ sickbeard.CACHE_IMAGE_URL_LIST.add_url(img_uri)
+ title = re.sub(r'(?i)(?::\s*season\s*\d+|\s*\((?:19|20)\d{2}\))?$', '', title.strip())
+
+ dt_ordinal = 0
+ dt_string = ''
+ date = genre = network = ''
+ date_tag = row.find('span', class_='startz')
+ if date_tag:
+ date_network = re.split(r'(?i)\son\s', ''.join(
+ [t.name and t.get_text() or str(t) for t in date_tag][0:2]))
+ date = re.sub('(?i)^(starts|returns)', '', date_network[0]).strip()
+ network = ('', date_network[1].strip())[2 == len(date_network)]
+ else:
+ date_tag = row.find('span', class_='selby')
+ if date_tag:
+ date = date_tag.get_text(strip=True)
+ network_genre = info.find('span')
+ if network_genre:
+ network_genre = network_genre.get_text(strip=True).split('//')
+ network = network_genre[0]
+ genre = ('', network_genre[1].strip())[2 == len(network_genre)]
+
+ if date:
+ dt = dateutil.parser.parse(date)
+ dt_ordinal = dt.toordinal()
+ dt_string = SGDatetime.sbfdate(dt)
+ if dt_ordinal < oldest_dt:
+ oldest_dt = dt_ordinal
+ oldest = dt_string
+ if dt_ordinal > newest_dt:
+ newest_dt = dt_ordinal
+ newest = dt_string
+
+ overview = row.find('span', class_='shwtxt')
+ if overview:
+ overview = re.sub(r'(?sim)(.*?)(?:[.\s]*\*\*NOTE.*?)?(\.{1,3})$', r'\1\2',
+ overview.get_text(strip=True))
+ for cur_entities in batches:
+ overview = re.sub(r'and(%s)' % '|'.join(cur_entities), r'&\1', overview)
+
+ votes = None
+ votes_tag = row.find('span', class_='selby')
+ if votes_tag:
+ votes_tag = votes_tag.find('strong')
+ if votes_tag:
+ votes = re.sub(r'(?i)\s*users', '', votes_tag.get_text()).strip()
+ use_votes = True
+
+ filtered.append(dict(
+ premiered=dt_ordinal,
+ premiered_str=dt_string,
+ when_past=dt_ordinal < datetime.datetime.now().toordinal(),
+ genres=genre,
+ network=network or None,
+ ids=ids,
+ images='' if not img_uri else images,
+ overview='No overview yet' if not overview else helpers.xhtml_escape(overview[:250:]),
+ rating=None,
+ title=title,
+ url_src_db='https://www.pogdesign.co.uk/%s' % url_path.strip('/'),
+ votes=votes or 'n/a'))
+
+ except (AttributeError, IndexError, KeyError, TypeError):
+ continue
+
+ kwargs.update(dict(oldest=oldest, newest=newest))
+
+ kwargs.update(dict(footnote=footnote, use_ratings=False, use_votes=use_votes, show_header=True))
+
+ mode = kwargs.get('mode', '')
+ if mode:
+ func = 'tvc_%s' % mode
+ if callable(getattr(self, func, None)):
+ sickbeard.TVC_MRU = func
+ sickbeard.save_config()
+
+ return self.browse_shows(browse_type, browse_title, filtered, **kwargs)
+
+ def info_tvcalendar(self, ids, show_name):
+
+ return self.new_show('|'.join(['', '', '', show_name]), use_show_name=True)
+
def mc_default(self):
return self.redirect('/add-shows/%s' % ('mc_newseries', sickbeard.MC_MRU)[any(sickbeard.MC_MRU)])