Browse Source

Merge branch 'feature/AddTVmazeCards' into develop

tags/release_0.25.1
JackDandy 4 years ago
parent
commit
d7363ce78d
  1. 3
      CHANGES.md
  2. 3
      gui/slick/css/style.css
  3. 85
      gui/slick/interfaces/default/home_browseShows.tmpl
  4. 4
      gui/slick/interfaces/default/inc_top.tmpl
  5. 2
      lib/imdb_api/imdb_api.py
  6. 2
      lib/libtrakt/indexerapiinterface.py
  7. 2
      lib/tmdb_api/tmdb_api.py
  8. 26
      lib/tvinfo_base/base.py
  9. 207
      lib/tvmaze_api/tvmaze_api.py
  10. 9
      sickbeard/__init__.py
  11. 2
      sickbeard/indexers/indexer_config.py
  12. 12
      sickbeard/tv.py
  13. 159
      sickbeard/webserve.py

3
CHANGES.md

@ -79,6 +79,9 @@
* Add fetch extra data fallback from TMDB for persons
* Change fanart icon
* Add provider TorrentDB
* Add menu Shows/"TVmaze Cards"
* Add show name/networks card user input filter
* Change only auto refresh card view if a recoverable error occurs
[develop changelog]

3
gui/slick/css/style.css

@ -707,7 +707,8 @@ inc_top.tmpl
}
.sgicon-tvmaze:before{
content:"\e89a"
content:"\e89a";
margin-right:14px
}
.sgicon-emby:before{

85
gui/slick/interfaces/default/home_browseShows.tmpl

@ -8,6 +8,8 @@
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
#set $mode = $kwargs and $kwargs.get('mode', '')
#set $use_network = $kwargs.get('use_networks', False)
#set $use_returning = 'returning' == mode
#set $use_votes = $kwargs and $kwargs.get('use_votes', True)
#set $use_ratings = $kwargs and $kwargs.get('use_ratings', True)
##
@ -20,7 +22,11 @@
##
#import os.path
#include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl')
<script>
var config = {
homeSearchFocus: #echo ['!1','!0'][$sg_var('HOME_SEARCH_FOCUS', True)]#,
};
</script>
<script type="text/javascript" src="$sg_root/js/plotTooltip.js?v=$sbPID"></script>
<script type="text/javascript" charset="utf-8">
@ -78,6 +84,13 @@ $(document).ready(function(){
#end if
return name.toLowerCase();
},
#if $use_returning
#raw
returning: function( itemElem ) {
return $( itemElem ).attr('data-returning') || '';
},
#end raw
#end if
#if $use_ratings
rating: '[data-rating] parseFloat',
#end if
@ -98,6 +111,9 @@ $(document).ready(function(){
#end raw
case 'order':
case 'premiered':
#if $use_returning
case 'returning':
#end if
#if $use_votes
case 'votes':
#end if
@ -222,7 +238,8 @@ $(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('tvc_') && -1 == filterValue.indexOf('ne_') && -1 == filterValue.indexOf('_ne')
&& -1 == filterValue.indexOf('tvc_') && -1 == filterValue.indexOf('tvm_')
&& -1 == filterValue.indexOf('ne_') && -1 == filterValue.indexOf('_ne')
&& -1 == filterValue.indexOf('default')) {
var el$ = $('#container')
el$.on('layoutComplete', llUpdate);
@ -241,6 +258,34 @@ $(document).ready(function(){
});
$('.service, .browse-image').each(addQTip);
if (config.homeSearchFocus) {
$('#search_show_name').focus();
}
llUpdate = (function(){
$.ll.handleScroll();
});
$('#search_show_name').on('input', function() {
var obj = $('#container');
obj.one('layoutComplete', llUpdate);
obj.isotope({
filter: function () {
var reSearch = RegExp($('#search_show_name').val(), 'i');
return reSearch.test($(this).attr('data-name'))#end raw##slurp#
#if $use_network#
|| reSearch.test(\$(this).attr('data-network'))#end if#;
#raw
}
});
});
$('.resetshows').click(function() {
var input = $('#search_show_name');
if ('' !== input.val()){
input.val('').trigger('input').change();
if (config.homeSearchFocus)
input.focus();
}
});
});
#end raw
@ -279,8 +324,14 @@ $(document).ready(function(){
</optgroup>
<optgroup label="Sort by">
<option value="by_name"#if 'by_name' in $saved_showsort_sortby and not $reset_showsort_sortby#$selected>>&nbsp;#else#>#end if#Name</option>
# omit for TVMaze as Original == First Aired
#if 'TVmaze' not in $browse_type
<option value="by_order"#if 'by_order' in $saved_showsort_sortby or $reset_showsort_sortby#$selected>>&nbsp;#else#>#end if#Original</option>
#end if
<option value="by_premiered"#if 'by_premiered' in $saved_showsort_sortby and not $reset_showsort_sortby#$selected>>&nbsp;#else#>#end if#First aired</option>
#if $use_returning
<option value="by_returning"#if 'by_returning' in $saved_showsort_sortby and not $reset_showsort_sortby#$selected>>&nbsp;#else#>#end if#Returning</option>
#end if
#if $use_votes
<option value="by_votes"#if 'by_votes' in $saved_showsort_sortby#$selected>>&nbsp;#else#>#end if#Votes</option>
#end if
@ -381,6 +432,11 @@ $(document).ready(function(){
#end for
<option value="tvc_latest"#echo ('', selected)['latest' == $mode]#>Latest additions</option>
</optgroup>
#elif 'TVmaze' == $browse_type
<optgroup label="TVmaze">
<option value="tvm_premieres"#echo ('', selected)['premieres' == $mode]#>Premieres</option>
<option value="tvm_returning"#echo ('', selected)['returning' == $mode]#>Returning</option>
</optgroup>
#elif 'Nextepisode' == $browse_type
<optgroup label="Nextepisode">
<option value="ne_newpop"#echo ('', selected)['newpop' == $mode]#>Popular premiered</option>
@ -393,9 +449,13 @@ $(document).ready(function(){
</select>
#end if
</div>
<div class="pull-right" style="clear:right">
<input id="search_show_name" class="search form-control form-control-inline input-sm input200" type="search" placeholder="Filter Show Name#if $use_network#/Network#end if#">
&nbsp;<button type="button" class="resetshows btn btn-inline">Reset Filter</button>
</div>
<h4 style="float:left;margin:0 0 0 2px">$browse_title</h4>
#if $kwargs and $kwargs.get('oldest')
<div class="grey-text" style="clear:both;margin-left:2px;font-size:0.85em">
<div class="grey-text" style="clear:left;margin-left:2px;font-size:0.85em">
First aired from $kwargs['oldest'] until $kwargs['newest']
</div>
#end if
@ -421,11 +481,14 @@ $(document).ready(function(){
#set $hide = ('', '%shide ' % ('', 'to-')['.hide' in $saved_showsort_view])[bool($this_show.get('hide'))]
#set $data_rating = $try_float($this_show['rating'])
<div class="show-card ${hide}${known}inlibrary" data-name="#echo re.sub(r'([\'\"])', r'', $this_show['title'])#" data_id="$show_id"#if $use_ratings# data-rating="$data_rating"#end if##if $use_votes# data-votes="$this_show['votes']"#end if# data-premiered="$this_show['premiered']" data-order="$this_show['order']">
<div class="show-card ${hide}${known}inlibrary" data-name="#echo re.sub(r'([\'\"])', r'', $this_show['title'])#" data_id="$show_id"#if $use_ratings# data-rating="$data_rating"#end if##if $use_votes# data-votes="$this_show['votes']"#end if# data-premiered="$this_show['premiered']"#if $use_returning# data-returning="$this_show['returning']"#end if# data-order="$this_show['order']"#if $use_network# data-network="$this_show['network']"#end if#>
<div class="show-card-inner">
<div class="browse-image">
<a class="browse-image" href="<%= anon_url(this_show['url_src_db']) %>" target="_blank"
title="<span style='color: rgb(66, 139, 202)'>$re.sub(r'(?m)\s+\((?:19|20)\d\d\)\s*$', '', $title_html)</span>#if $this_show['genres']#<br /><div style='font-weight:bold'>(<em>$this_show['genres']</em>)</div>#end if#
title="<span style='color: rgb(66, 139, 202)'>$re.sub(r'(?m)\s+\((?:19|20)\d\d\)\s*$', '', $title_html)</span>
#if $this_show['genres']#<br><div style='font-weight:bold'>(<em>$this_show['genres']</em>)</div>#end if#
#if $kwargs and $use_returning#<span style='font-weight:bold;font-size:0.9em;color:#888'><em>Season $this_show['episode_season'] returns $this_show['returning_str']</em></span>#end if#
#if $this_show.get('country') or $this_show.get('language')
<p style='line-height:15px;margin-bottom:2px'>
#if $this_show.get('country')
@ -496,18 +559,18 @@ $(document).ready(function(){
$kwargs['error_msg']
#else
$browse_type did not return results, this can happen from time to time.
<br /><br />This view should auto refresh every 10 mins.
#end if
</p>
</div>
</div>
#end if
<br><br>This view should auto refresh every 10 mins.
<script type="text/javascript" charset="utf-8">
<!--
window.setInterval('location.reload(true)', 600000); // Refresh every 10 minutes
//-->
</script>
#end if
</p>
</div>
</div>
#end if
<script type="text/javascript" src="$sg_root/js/lazyload/lazyload.min.js?v=$sbPID"></script>
<script type="text/javascript" src="$sg_root/js/inc_bottom.js?v=$sbPID"></script>

4
gui/slick/interfaces/default/inc_top.tmpl

@ -188,6 +188,10 @@
#set $tvc_mode = $tvc_modes.get($sg_var('TVC_MRU'), 'new shows')
<li><a href="$sbRoot/add-shows/tvc-default/" tabindex="$tab#set $tab += 1#"><i class="sgicon-tvc"></i>TV Calendar Cards
<div class="menu-item-desc opacity60">$tvc_mode...</div></a></li>
#set $tvm_modes = dict(tvm_premieres='new shows', tvm_returning='returning')
#set $tvm_mode = $tvm_modes.get($sg_var('TVM_MRU'), 'new shows')
<li><a href="$sbRoot/add-shows/tvm-default/" tabindex="$tab#set $tab += 1#"><i class="sgicon-tvmaze"></i>TVmaze Cards
<div class="menu-item-desc opacity60">$tvm_mode...</div></a></li>
#set $ne_modes = dict(ne_newpop='new popular', ne_newtop='new top rated', ne_upcoming='upcoming', ne_trending='trending')
#set $ne_mode = $ne_modes.get($sg_var('NE_MRU'), 'new popular')
<li><a href="$sbRoot/add-shows/ne-default/" tabindex="$tab#set $tab += 1#"><i class="sgicon-ne"></i>Next Episode Cards

2
lib/imdb_api/imdb_api.py

@ -80,7 +80,7 @@ class IMDbIndexer(TVInfoBase):
is_none, shows = self._get_cache_entry(cache_id_key)
if not self.config.get('cache_search') or (None is shows and not is_none):
try:
show = imdbpie.Imdb().get_title_auxiliary('tt%07d' % p)
show = imdbpie.Imdb().get_title_auxiliary('tt%08d' % p)
except (BaseException, Exception):
continue
self._set_cache_entry(cache_id_key, show, expire=self.search_cache_expire)

2
lib/libtrakt/indexerapiinterface.py

@ -185,7 +185,7 @@ class TraktIndexer(TVInfoBase):
if TraktSearchTypes.trakt_slug == search_type:
url = '/shows/%s?extended=full' % series
elif TraktSearchTypes.text != search_type:
url = '/search/%s/%s?type=%s&extended=full&limit=100' % (search_type, (series, 'tt%07d' % series)[
url = '/search/%s/%s?type=%s&extended=full&limit=100' % (search_type, (series, 'tt%08d' % series)[
TraktSearchTypes.imdb_id == search_type and not str(series).startswith('tt')],
','.join(self.config['result_types']))
else:

2
lib/tmdb_api/tmdb_api.py

@ -210,7 +210,7 @@ class TmdbIndexer(TVInfoBase):
is_none, shows = self._get_cache_entry(cache_id_key)
if not self.config.get('cache_search') or (None is shows and not is_none):
try:
show = tmdbsimple.Find(id=(p, 'tt%07d' % p)[t == TVINFO_IMDB]).info(
show = tmdbsimple.Find(id=(p, 'tt%08d' % p)[t == TVINFO_IMDB]).info(
external_source=id_map[t])
if show.get('tv_results') and 1 == len(show['tv_results']):
show = tmdbsimple.TV(id=show['tv_results'][0]['id']).info(

26
lib/tvinfo_base/base.py

@ -126,6 +126,9 @@ class TVInfoIDs(object):
return {TVINFO_TVDB: self.tvdb, TVINFO_TMDB: self.tmdb, TVINFO_TVMAZE: self.tvmaze,
TVINFO_IMDB: self.imdb, TVINFO_TRAKT: self.trakt, TVINFO_TVRAGE: self.rage}.get(key)
def get(self, key):
return self.__getitem__(key)
def __iter__(self):
for s, v in [(TVINFO_TVDB, self.tvdb), (TVINFO_TMDB, self.tmdb), (TVINFO_TVMAZE, self.tvmaze),
(TVINFO_IMDB, self.imdb), (TVINFO_TRAKT, self.trakt), (TVINFO_TVRAGE, self.rage)]:
@ -502,14 +505,16 @@ class TVInfoEpisode(dict):
self.thumbadded = None # type: Optional[AnyStr]
self.rating = None # type: Union[integer_types, float]
self.siteratingcount = None # type: integer_types
self.show = None # type: Optional[TVInfoShow]
def __str__(self):
show_name = self.show and self.show.seriesname and '<Show %s> - ' % self.show.seriesname
seasno, epno = int(getattr(self, 'seasonnumber', 0)), int(getattr(self, 'episodenumber', 0))
epname = getattr(self, 'episodename', '')
if None is not epname:
return '<Episode %02dx%02d - %r>' % (seasno, epno, epname)
return '%s<Episode %02dx%02d - %r>' % (show_name, seasno, epno, epname)
else:
return '<Episode %02dx%02d>' % (seasno, epno)
return '%s<Episode %02dx%02d>' % (show_name, seasno, epno)
def __getattr__(self, key):
if key in self:
@ -554,6 +559,7 @@ class TVInfoEpisode(dict):
if cur_value.find(text_type(term).lower()) > -1:
return self
__unicode__ = __str__
__repr__ = __str__
__nonzero__ = __bool__
@ -1146,8 +1152,22 @@ class TVInfoBase(object):
"""
return []
def discover(self, result_count=100, **kwargs):
def discover(self, result_count=100, get_extra_images=False, **kwargs):
# type: (...) -> List[TVInfoEpisode]
return []
def get_premieres(self, result_count=100, **kwargs):
# type: (...) -> List[TVInfoEpisode]
"""
get all premiering shows
"""
return []
def get_returning(self, result_count=100, get_extra_images=False, **kwargs):
# type: (...) -> List[TVInfoShow]
"""
get all returning shows
"""
return []
def __getitem__(self, item):

207
lib/tvmaze_api/tvmaze_api.py

@ -6,18 +6,21 @@ __author__ = 'Prinz23'
__version__ = '1.0'
__api_version__ = '1.0.0'
import logging
import datetime
import logging
import re
import requests
from requests.packages.urllib3.util.retry import Retry
from urllib3.util.retry import Retry
from requests.adapters import HTTPAdapter
from six import iteritems
from six import integer_types, iteritems
from sg_helpers import get_url, try_int
from lib.dateutil.parser import parser
# noinspection PyProtectedMember
from lib.dateutil.tz.tz import _datetime_to_timestamp
from lib.exceptions_helper import ConnectionSkipException, ex
from .tvmaze_exceptions import *
# from .tvmaze_exceptions import *
from lib.tvinfo_base import TVInfoBase, TVInfoImage, TVInfoImageSize, TVInfoImageType, Character, Crew, \
crew_type_names, Person, RoleTypes, TVInfoShow, TVInfoEpisode, TVInfoIDs, TVInfoSeason, PersonGenders, \
TVINFO_TVMAZE, TVINFO_TVDB, TVINFO_IMDB
@ -25,8 +28,8 @@ from lib.pytvmaze import tvmaze
# noinspection PyUnreachableCode
if False:
from typing import Any, AnyStr, Dict, List, Optional, Union
from six import integer_types
from typing import Any, AnyStr, Dict, List, Optional
from lib.pytvmaze.tvmaze import Episode as TVMazeEpisode, Show as TVMazeShow
log = logging.getLogger('tvmaze.api')
log.addHandler(logging.NullHandler())
@ -38,8 +41,10 @@ def tvmaze_endpoint_standard_get(url):
retries = Retry(total=5,
backoff_factor=0.1,
status_forcelist=[429])
# noinspection HttpUrlsUsage
s.mount('http://', HTTPAdapter(max_retries=retries))
s.mount('https://', HTTPAdapter(max_retries=retries))
# noinspection PyProtectedMember
return get_url(url, json=True, session=s, hooks={'response': tvmaze._record_hook}, raise_skip_exception=True)
@ -140,9 +145,9 @@ class TvMaze(TVInfoBase):
'network': s.network and s.network.name,
'genres': s.genres, 'overview': s.summary,
'aliases': [a.name for a in s.akas], 'image': s.image and s.image.get('original'),
'ids': TVInfoIDs(tvdb=s.externals.get('thetvdb'), rage=s.externals.get('tvrage'), tvmaze=s.id,
imdb=s.externals.get('imdb') and try_int(s.externals.get('imdb').replace('tt', ''),
None))}
'ids': TVInfoIDs(
tvdb=s.externals.get('thetvdb'), rage=s.externals.get('tvrage'), tvmaze=s.id,
imdb=s.externals.get('imdb') and try_int(s.externals.get('imdb').replace('tt', ''), None))}
results = []
if ids:
for t, p in iteritems(ids):
@ -161,7 +166,7 @@ class TvMaze(TVInfoBase):
elif t == TVINFO_IMDB:
if not self.config.get('cache_search') or (None is shows and not is_none):
try:
show = tvmaze.lookup_imdb((p, 'tt%07d' % p)[not str(p).startswith('tt')])
show = tvmaze.lookup_imdb((p, 'tt%08d' % p)[not str(p).startswith('tt')])
self._set_cache_entry(cache_id_key, show, expire=self.search_cache_expire)
except (BaseException, Exception):
continue
@ -201,11 +206,11 @@ class TvMaze(TVInfoBase):
return results
def _set_episode(self, sid, ep_obj):
for _k, _s in [('seasonnumber', 'season_number'), ('episodenumber', 'episode_number'),
for _k, _s in (
('seasonnumber', 'season_number'), ('episodenumber', 'episode_number'),
('episodename', 'title'), ('overview', 'summary'), ('firstaired', 'airdate'),
('airtime', 'airtime'), ('runtime', 'runtime'),
('seriesid', 'maze_id'), ('id', 'maze_id'), ('is_special', 'special'),
('filename', 'image')]:
('seriesid', 'maze_id'), ('id', 'maze_id'), ('is_special', 'special'), ('filename', 'image')):
if 'filename' == _k:
image = getattr(ep_obj, _s, {}) or {}
image = image.get('original') or image.get('medium')
@ -221,7 +226,8 @@ class TvMaze(TVInfoBase):
except (BaseException, Exception):
pass
def _set_network(self, show_obj, network, is_stream):
@staticmethod
def _set_network(show_obj, network, is_stream):
show_obj['network'] = network.name
show_obj['network_timezone'] = network.timezone
show_obj['network_country'] = network.country
@ -229,17 +235,21 @@ class TvMaze(TVInfoBase):
show_obj['network_id'] = network.maze_id
show_obj['network_is_stream'] = is_stream
def _get_show_data(self, sid, language, get_ep_info=False, banners=False, posters=False, seasons=False,
seasonwides=False, fanart=False, actors=False, **kwargs):
log.debug('Getting all series data for %s' % sid)
def _get_tvm_show(self, show_id, get_ep_info):
try:
self.show_not_found = False
show_data = tvm_obj.get_show(maze_id=sid, embed='cast%s' % ('', ',episodeswithspecials')[get_ep_info])
return tvm_obj.get_show(maze_id=show_id, embed='cast%s' % ('', ',episodeswithspecials')[get_ep_info])
except tvmaze.ShowNotFound:
self.show_not_found = True
return False
except (BaseException, Exception) as e:
log.debug('Error getting data for tvmaze show id: %s' % sid)
except (BaseException, Exception):
log.debug('Error getting data for tvmaze show id: %s' % show_id)
def _get_show_data(self, sid, language, get_ep_info=False, banners=False, posters=False, seasons=False,
seasonwides=False, fanart=False, actors=False, **kwargs):
log.debug('Getting all series data for %s' % sid)
show_data = self._get_tvm_show(sid, get_ep_info)
if not show_data:
return False
show_obj = self.shows[sid].__dict__
@ -330,19 +340,20 @@ class TvMaze(TVInfoBase):
except (BaseException, Exception):
print('error')
pass
existing_person.p_id, existing_person.name, existing_person.image, existing_person.gender, \
existing_person.birthdate, existing_person.deathdate, existing_person.country, \
existing_person.country_code, existing_person.country_timezone, existing_person.thumb_url, \
existing_person.url, existing_person.ids = \
ch.person.id, ch.person.name, ch.person.image and ch.person.image.get('original'), \
PersonGenders.named.get(ch.person.gender and ch.person.gender.lower(),
PersonGenders.unknown),\
person.birthdate, person.deathdate,\
ch.person.country and ch.person.country.get('name'),\
ch.person.country and ch.person.country.get('code'),\
ch.person.country and ch.person.country.get('timezone'),\
ch.person.image and ch.person.image.get('medium'),\
ch.person.url, {TVINFO_TVMAZE: ch.person.id}
(existing_person.p_id, existing_person.name, existing_person.image, existing_person.gender,
existing_person.birthdate, existing_person.deathdate, existing_person.country,
existing_person.country_code, existing_person.country_timezone, existing_person.thumb_url,
existing_person.url, existing_person.ids) = \
(ch.person.id, ch.person.name,
ch.person.image and ch.person.image.get('original'),
PersonGenders.named.get(
ch.person.gender and ch.person.gender.lower(), PersonGenders.unknown),
person.birthdate, person.deathdate,
ch.person.country and ch.person.country.get('name'),
ch.person.country and ch.person.country.get('code'),
ch.person.country and ch.person.country.get('timezone'),
ch.person.image and ch.person.image.get('medium'),
ch.person.url, {TVINFO_TVMAZE: ch.person.id})
else:
existing_character.person.append(person)
else:
@ -406,15 +417,8 @@ class TvMaze(TVInfoBase):
if get_ep_info and not getattr(self.shows.get(sid), 'ep_loaded', False):
log.debug('Getting all episodes of %s' % sid)
if None is show_data:
try:
self.show_not_found = False
show_data = tvm_obj.get_show(maze_id=sid, embed='cast%s' % ('', ',episodeswithspecials')[
get_ep_info])
except tvmaze.ShowNotFound:
self.show_not_found = True
return False
except (BaseException, Exception) as e:
log.debug('Error getting data for tvmaze show id: %s' % sid)
show_data = self._get_tvm_show(sid, get_ep_info)
if not show_data:
return False
if show_data.episodes:
@ -543,3 +547,120 @@ class TvMaze(TVInfoBase):
p = None
if p:
return self._convert_person(p)
def get_premieres(self, result_count=100, get_extra_images=False, **kwargs):
# type: (...) -> List[TVInfoEpisode]
return self._filtered_schedule(lambda e: all([1 == e.season_number, 1 == e.episode_number]),
get_images=get_extra_images)
def get_returning(self, result_count=100, get_extra_images=False, **kwargs):
# type: (...) -> List[TVInfoEpisode]
return self._filtered_schedule(lambda e: all([1 != e.season_number, 1 == e.episode_number]),
get_images=get_extra_images)
def _make_episode(self, episode_data, show_data=None, get_images=False):
# type: (TVMazeEpisode, TVMazeShow, bool) -> TVInfoEpisode
"""
make out of TVMazeEpisode object and optionally TVMazeShow a TVInfoEpisode
"""
ti_show = TVInfoShow()
ti_show.seriesname = show_data.name
ti_show.id = show_data.maze_id
ti_show.seriesid = ti_show.id
ti_show.language = show_data.language
ti_show.overview = show_data.summary
ti_show.firstaired = show_data.premiered
ti_show.runtime = show_data.average_runtime or show_data.runtime
ti_show.vote_average = show_data.rating and show_data.rating.get('average')
ti_show.popularity = show_data.weight
ti_show.genre_list = show_data.genres or []
ti_show.genre = ', '.join(ti_show.genre_list)
ti_show.official_site = show_data.official_site
ti_show.status = show_data.status
ti_show.show_type = show_data.type
ti_show.lastupdated = show_data.updated
ti_show.poster = show_data.image and show_data.image.get('original')
ti_show.aliases = [a.name for a in show_data.akas]
if 'days' in show_data.schedule:
ti_show.airs_dayofweek = ', '.join(show_data.schedule['days'])
network = show_data.network or show_data.web_channel
if network:
ti_show.network_is_stream = None is not show_data.web_channel
ti_show.network = network.name
ti_show.network_id = network.maze_id
ti_show.network_country = network.country
ti_show.network_country_code = network.code
ti_show.network_timezone = network.timezone
if get_images and show_data.images:
b_set, f_set, p_set = False, False, False
for cur_img in show_data.images:
img_type = img_type_map.get(cur_img.type, TVInfoImageType.other)
img_width, img_height = cur_img.resolutions['original'].get('width'), \
cur_img.resolutions['original'].get('height')
img_ar = img_width and img_height and float(img_width) / float(img_height)
img_ar_type = self._which_type(img_width, img_ar)
if TVInfoImageType.poster == img_type and img_ar and img_ar_type != img_type and \
ti_show.poster == cur_img.resolutions.get('original')['url']:
p_set = False
ti_show.poster = None
ti_show.poster_thumb = None
img_type = (TVInfoImageType.other, img_type)[
not img_ar or img_ar_type == img_type or
img_type not in (TVInfoImageType.banner, TVInfoImageType.poster, TVInfoImageType.fanart)]
img_src = {}
for cur_res, cur_img_url in iteritems(cur_img.resolutions):
img_size = img_size_map.get(cur_res)
if img_size:
img_src[img_size] = cur_img_url.get('url')
ti_show.images.setdefault(img_type, []).append(
TVInfoImage(
image_type=img_type, sizes=img_src, img_id=cur_img.id, main_image=cur_img.main,
type_str=cur_img.type, width=img_width, height=img_height, aspect_ratio=img_ar))
if not p_set and TVInfoImageType.poster == img_type:
p_set = True
ti_show.poster = cur_img.resolutions.get('original')['url']
ti_show.poster_thumb = cur_img.resolutions.get('original')['url']
elif not b_set and 'banner' == cur_img.type and TVInfoImageType.banner == img_type:
b_set = True
ti_show.banner = cur_img.resolutions.get('original')['url']
ti_show.banner_thumb = cur_img.resolutions.get('medium')['url']
elif not f_set and 'background' == cur_img.type and TVInfoImageType.fanart == img_type:
f_set = True
ti_show.fanart = cur_img.resolutions.get('original')['url']
ti_show.ids = TVInfoIDs(
tvdb=show_data.externals.get('thetvdb'), rage=show_data.externals.get('tvrage'), tvmaze=show_data.id,
imdb=show_data.externals.get('imdb') and try_int(show_data.externals.get('imdb').replace('tt', ''), None))
ti_show.imdb_id = show_data.externals.get('imdb')
if isinstance(ti_show.imdb_id, integer_types):
ti_show.imdb_id = 'tt%08d' % ti_show.imdb_id
ti_episode = TVInfoEpisode()
ti_episode.id = episode_data.maze_id
ti_episode.seasonnumber = episode_data.season_number
ti_episode.episodenumber = episode_data.episode_number
ti_episode.episodename = episode_data.title
ti_episode.airtime = episode_data.airtime
ti_episode.firstaired = episode_data.airdate
if episode_data.airstamp:
try:
at = _datetime_to_timestamp(tz_p.parse(episode_data.airstamp))
ti_episode.timestamp = at
except (BaseException, Exception):
pass
ti_episode.filename = episode_data.image and (episode_data.image.get('original') or
episode_data.image.get('medium'))
ti_episode.is_special = episode_data.is_special()
ti_episode.overview = episode_data.summary
ti_episode.runtime = episode_data.runtime
ti_episode.show = ti_show
return ti_episode
def _filtered_schedule(self, condition, get_images=False):
try:
result = sorted([
e for e in tvmaze.get_full_schedule()
if condition(e) and (None is e.show.language or re.search('(?i)eng|jap', e.show.language))],
key=lambda x: x.show.premiered or x.airstamp)
return [self._make_episode(r, r.show, get_images) for r in result]
except(BaseException, Exception):
return []

9
sickbeard/__init__.py

@ -618,6 +618,7 @@ else:
MC_MRU = ''
TVC_MRU = ''
TVM_MRU = ''
NE_MRU = ''
COOKIE_SECRET = b64encodestring(uuid.uuid4().bytes + uuid.uuid4().bytes)
@ -763,7 +764,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, TVC_MRU, NE_MRU, \
MC_MRU, TVC_MRU, TVM_MRU, NE_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_DISCORD, DISCORD_NOTIFY_ONSNATCH, DISCORD_NOTIFY_ONDOWNLOAD, \
@ -1207,6 +1208,7 @@ def init_stage_1(console_logging):
MC_MRU = check_setting_str(CFG, 'Metacritic', 'mc_mru', '')
TVC_MRU = check_setting_str(CFG, 'TVCalendar', 'tvc_mru', '')
TVM_MRU = check_setting_str(CFG, 'TVmaze', 'tvm_mru', '')
NE_MRU = check_setting_str(CFG, 'NextEpisode', 'ne_mru', '')
USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0))
@ -1692,7 +1694,7 @@ def init_stage_2():
run_delay=datetime.timedelta(minutes=5),
threadName='PLEXWATCHEDSTATE')
MEMCACHE['history_tab_limit'] = 10
MEMCACHE['history_tab_limit'] = 11
MEMCACHE['history_tab'] = History.menu_tab(MEMCACHE['history_tab_limit'])
try:
@ -2212,6 +2214,9 @@ def save_config():
('TVCalendar', [
('mru', TVC_MRU)
]),
('TVmaze', [
('mru', TVM_MRU)
]),
('NextEpisode', [
('mru', NE_MRU)
]),

2
sickbeard/indexers/indexer_config.py

@ -176,7 +176,7 @@ tvinfo_config[src].update(dict(
src = TVINFO_IMDB
tvinfo_config[src].update(dict(
base_url=tvinfo_config[src]['main_url'],
show_url='%stitle/tt%%07d' % tvinfo_config[src]['main_url'],
show_url='%stitle/tt%%08d' % tvinfo_config[src]['main_url'],
finder='%sfind?q=%s&s=tt&ttype=tv&ref_=fn_tv' % (tvinfo_config[src]['main_url'], '%s'),
))

12
sickbeard/tv.py

@ -2757,7 +2757,7 @@ class TVShow(TVShowBase):
old_imdb = self.imdbid
if show_info.ids.imdb:
self.imdbid = 'tt%07d' % show_info.ids.imdb
self.imdbid = 'tt%08d' % show_info.ids.imdb
else:
self.imdbid = self.dict_prevent_nonetype(show_info, 'imdb_id')
if old_imdb != self.imdbid:
@ -2977,7 +2977,7 @@ class TVShow(TVShowBase):
if not self._imdbid and 0 >= self.ids.get(indexermapper.TVINFO_IMDB, {'id': 0}).get('id', 0):
return
imdb_info = {'imdb_id': self._imdbid or 'tt%07d' % self.ids[indexermapper.TVINFO_IMDB]['id'],
imdb_info = {'imdb_id': self._imdbid or 'tt%08d' % self.ids[indexermapper.TVINFO_IMDB]['id'],
'title': '',
'year': '',
'akas': '',
@ -2995,7 +2995,7 @@ class TVShow(TVShowBase):
imdb_id = None
imdb_certificates = None
try:
imdb_id = str(self._imdbid or 'tt%07d' % self.ids[indexermapper.TVINFO_IMDB]['id'])
imdb_id = str(self._imdbid or 'tt%08d' % self.ids[indexermapper.TVINFO_IMDB]['id'])
redirect_check = self.check_imdb_redirect(imdb_id)
if redirect_check:
self._imdbid = redirect_check
@ -3016,15 +3016,15 @@ class TVShow(TVShowBase):
})
imdb_certificates = i.get_title_certificates(imdb_id=imdb_id)
except LookupError as e:
if 'Title was an episode' in ex(e) and imdb_id == 'tt%07d' % self.ids[indexermapper.TVINFO_IMDB]['id']:
if 'Title was an episode' in ex(e) and imdb_id == 'tt%08d' % self.ids[indexermapper.TVINFO_IMDB]['id']:
self.ids[indexermapper.TVINFO_IMDB]['id'] = 0
self.ids[indexermapper.TVINFO_IMDB]['status'] = MapStatus.NOT_FOUND
if datetime.date.today() != self.ids[indexermapper.TVINFO_IMDB]['date']:
indexermapper.map_indexers_to_show(self, force=True)
if not retry and imdb_id != 'tt%07d' % self.ids[indexermapper.TVINFO_IMDB]['id']:
if not retry and imdb_id != 'tt%08d' % self.ids[indexermapper.TVINFO_IMDB]['id']:
# add retry arg to prevent endless loops
logger.log('imdbid: %s not found. retrying with newly found id: %s' %
(imdb_id, 'tt%07d' % self.ids[indexermapper.TVINFO_IMDB]['id']), logger.DEBUG)
(imdb_id, 'tt%08d' % self.ids[indexermapper.TVINFO_IMDB]['id']), logger.DEBUG)
self._get_imdb_info(retry=True)
return
logger.log('imdbid: %s not found. Error: %s' % (imdb_id, ex(e)), logger.WARNING)

159
sickbeard/webserve.py

@ -5168,6 +5168,159 @@ class AddShows(Home):
return self.new_show('|'.join(['', '', '', show_name]), use_show_name=True)
def tvm_default(self):
return self.redirect('/add-shows/%s' % ('tvm_premieres', sickbeard.TVM_MRU)[any(sickbeard.TVM_MRU)])
def tvm_premieres(self, **kwargs):
return self.browse_tvm(
'New Shows at TVmaze', mode='premieres', **kwargs)
def tvm_returning(self, **kwargs):
return self.browse_tvm(
'Returning Shows at TVmaze', mode='returning', **kwargs)
def browse_tvm(self, browse_title, **kwargs):
browse_type = 'TVmaze'
footnote = None
filtered = []
def card_cache(mem_key):
# noinspection PyProtectedMember
from lib.dateutil.tz.tz import _datetime_to_timestamp
now = int(_datetime_to_timestamp(datetime.datetime.now()))
if (sickbeard.MEMCACHE.get(mem_key, {}).get('data')
and (now < sickbeard.MEMCACHE.get(mem_key, {}).get('expire', 0))):
return sickbeard.MEMCACHE.get(mem_key).get('data')
tvinfo_config = sickbeard.TVInfoAPI(TVINFO_TVMAZE).api_params.copy()
t = sickbeard.TVInfoAPI(TVINFO_TVMAZE).setup(**tvinfo_config)
if 'prem' in mem_key:
data = t.get_premieres()
else:
data = t.get_returning()
sickbeard.MEMCACHE[mem_key] = dict(expire=(30*60) + now, data=data)
return data
if 'New' in browse_title:
episodes = card_cache('tvmaze_premiere')
else:
episodes = card_cache('tvmaze_returning')
oldest, newest, oldest_dt, newest_dt, use_networks = None, None, 9999999, 0, False
dedupe = []
parseinfo = dateutil.parser.parserinfo(dayfirst=False, yearfirst=True)
base_url = sickbeard.TVInfoAPI(TVINFO_TVMAZE).config['show_url']
for cur_episode_info in episodes:
if cur_episode_info.show.id in dedupe:
continue
dedupe += [cur_episode_info.show.id]
try:
if cur_episode_info.airtime:
airtime = dateutil.parser.parse(cur_episode_info.airtime).time()
else:
airtime = cur_episode_info.timestamp \
and SGDatetime.from_timestamp(cur_episode_info.timestamp).time()
if (0, 0) == (airtime.hour, airtime.minute):
airtime = dateutil.parser.parse('23:59').time()
dt = datetime.datetime.combine(
dateutil.parser.parse(
(cur_episode_info.show.firstaired or cur_episode_info.firstaired), parseinfo).date(), airtime)
dt_ordinal = dt.toordinal()
now_ordinal = datetime.datetime.now().toordinal()
when_past = dt_ordinal < now_ordinal
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
returning = returning_str = None
if 'Return' in browse_title:
returning = '9'
returning_str = 'TBC'
if cur_episode_info.firstaired:
returning = cur_episode_info.firstaired
dt_returning = datetime.datetime.combine(
dateutil.parser.parse(returning, parseinfo).date(), airtime)
when_past = dt_returning.toordinal() < now_ordinal
returning_str = SGDatetime.sbfdate(dt_returning)
try:
img_uri = cur_episode_info.show.poster
images = dict(poster=dict(thumb='imagecache?path=browse/thumb/tvmaze&source=%s' % img_uri))
sickbeard.CACHE_IMAGE_URL_LIST.add_url(img_uri)
except(BaseException, Exception):
images = {}
ids = dict(tvmaze=cur_episode_info.id)
imdb_id = cur_episode_info.show.imdb_id
if imdb_id:
ids['imdb'] = imdb_id
tvdb_id = cur_episode_info.show.ids.get(TVINFO_TVDB)
if tvdb_id:
ids['tvdb'] = tvdb_id
network_name = cur_episode_info.show.network
cc = 'US'
if network_name:
use_networks = True
cc = cur_episode_info.show.network_country_code or cc
language = ((cur_episode_info.show.language and 'jap' in cur_episode_info.show.language.lower())
and 'jp' or 'en')
filtered.append(dict(
ids=ids,
premiered=dt_ordinal,
premiered_str=dt_string,
returning=returning,
returning_str=returning_str,
when_past=when_past,
episode_number=cur_episode_info.episodenumber,
episode_season=cur_episode_info.seasonnumber,
episode_overview='' if not cur_episode_info.overview else cur_episode_info.overview.strip(),
genres=(', '.join(['%s' % v for v in cur_episode_info.show.genre_list])
or cur_episode_info.show.show_type or ''),
images=images,
overview=('No overview yet' if not cur_episode_info.show.overview
else helpers.xhtml_escape(cur_episode_info.show.overview.strip()[:250:])
.strip('*').strip()),
title=cur_episode_info.show.seriesname,
language=language,
language_img=sickbeard.MEMCACHE_FLAG_IMAGES.get(language, False),
country=cc,
country_img=sickbeard.MEMCACHE_FLAG_IMAGES.get(cc.lower(), False),
network=network_name,
rating=cur_episode_info.show.rating or cur_episode_info.show.popularity or 0,
url_src_db=base_url % cur_episode_info.show.id,
))
except (BaseException, Exception):
pass
kwargs.update(dict(oldest=oldest, newest=newest))
kwargs.update(dict(footnote=footnote, use_votes=False, use_networks=use_networks))
mode = kwargs.get('mode', '')
if mode:
func = 'tvm_%s' % mode
if callable(getattr(self, func, None)):
sickbeard.TVM_MRU = func
sickbeard.save_config()
return self.browse_shows(browse_type, browse_title, filtered, **kwargs)
# noinspection PyUnusedLocal
def info_tvmaze(self, ids, show_name):
if not filter_list(lambda tvid_prodid: helpers.find_show_by_id(tvid_prodid), ids.split(' ')):
return self.new_show('|'.join(['', '', '', ' '.join([ids, 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)])
@ -5525,7 +5678,7 @@ class AddShows(Home):
@staticmethod
def browse_mru(browse_type, **kwargs):
save_config = False
if browse_type in ('AniDB', 'IMDb', 'Metacritic', 'Trakt', 'TVCalendar', 'Nextepisode'):
if browse_type in ('AniDB', 'IMDb', 'Metacritic', 'Trakt', 'TVCalendar', 'TVmaze', 'Nextepisode'):
save_config = True
sickbeard.BROWSELIST_MRU[browse_type] = dict(
showfilter=kwargs.get('showfilter', ''), showsort=kwargs.get('showsort', ''))
@ -5547,7 +5700,8 @@ class AddShows(Home):
showsort = t.saved_showsort.split(',')
t.saved_showsort_sortby = 3 == len(showsort) and showsort[2] or 'by_order'
t.reset_showsort_sortby = ('votes' in t.saved_showsort_sortby and not kwargs.get('use_votes', True)
or 'rating' in t.saved_showsort_sortby and not kwargs.get('use_ratings', True))
or 'rating' in t.saved_showsort_sortby and not kwargs.get('use_ratings', True)
or 'returning' in t.saved_showsort_sortby and 'Returning' not in browse_title)
t.is_showsort_desc = ('desc' == (2 <= len(showsort) and showsort[1] or 'asc')) and not t.reset_showsort_sortby
t.saved_showsort_view = 1 <= len(showsort) and showsort[0] or '*'
t.all_shows = []
@ -5635,6 +5789,7 @@ class AddShows(Home):
('order', lambda _x: _x['order']),
('name', lambda _x: _title(_x['title'])),
('premiered', lambda _x: (_x['premiered'], _title(_x['title']))),
('returning', lambda _x: (_x['returning'], _title(_x['title']))),
('votes', lambda _x: (helpers.try_int(_x['votes']), _title(_x['title']))),
('rating', lambda _x: (helpers.try_float(_x['rating']), _title(_x['title']))),
('rating_votes', lambda _x: (helpers.try_float(_x['rating']), helpers.try_int(_x['votes']),

Loading…
Cancel
Save