#import datetime #import re ## #import sickbeard #from sickbeard import history, providers, sbdatetime, WEB_PORT #from sickbeard.common import Quality, statusStrings, SNATCHED_ANY, SNATCHED_PROPER, DOWNLOADED, SUBTITLED, ARCHIVED, FAILED #from sickbeard.helpers import human #from sickbeard.providers import generic <% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp# <% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp# ## #set $layout = $sg_str('HISTORY_LAYOUT', 'detailed') #set $layout_name = 'watched' in $layout and 'Watched' or 'stats' in $layout and 'Activity Hits' or 'provider_failures'in $layout and 'Provider Failures' or 'Activity' #set sg_port = str($getVar('sbHttpPort', WEB_PORT)) #set global $title = 'History : %s' % $layout_name #set global $header = 'History : %s' % $layout_name #set global $sbPath = '..' #set global $topmenu = 'home' ## #import os.path #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl') ## #set $checked = ' checked="checked"' #if $varExists('header')

$header

#else

$title

#end if #if $varExists('earliest')
Stats range from $sbdatetime.sbdatetime.sbfdatetime($datetime.datetime.strptime(str($earliest), $history.dateFormat)) until $sbdatetime.sbdatetime.sbfdatetime($datetime.datetime.strptime(str($latest), $history.dateFormat))
#end if #set $selected = ' selected="selected" class="selected"' ##
Layout:
## #if 'failure' not in $layout ## ## #end if ## ## #if 'detailed' == $layout ## ## #for $hItem in $history_results #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem['action'])) #set $display_name = '%s - S%02iE%02i' % ( $hItem['data_name'], (('%s %s' % ($hItem['name1'], $hItem['name2'])), $hItem['show_name'])[$sg_var('SORT_ARTICLE') or not $hItem['name1']], int($hItem['season']), int($hItem['episode'])) #set $curdatetime = $datetime.datetime.strptime(str($hItem['date']), $history.dateFormat) #if $SUBTITLED == $curStatus "> #end if $statusStrings[$curStatus].replace('SD DVD', 'SD DVD/BR/BD') #end for ## ## #elif ('compact' == $layout) ## ## #if $sg_var('USE_SUBTITLES') #end if #for $hItem in $compact_results #set $curdatetime = $datetime.datetime.strptime(str($hItem['actions'][0]['time']), $history.dateFormat) #set $display_name = '%s - S%02iE%02i' % ( $hItem['data_name'], (('%s %s' % ($hItem['name1'], $hItem['name2'])), $hItem['show_name'])[$sg_var('SORT_ARTICLE') or not $hItem['name1']], int($hItem['season']), int($hItem['episode'])) #set $prov_list = [] #set $down_list = [] #set $order = 1 #set $ordinal_indicators = {'1':'st', '2':'nd', '3':'rd'} #for $action in reversed($hItem['actions']) #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action'])) #set $basename = $os.path.basename($action['resource']) #if $curStatus in $SNATCHED_ANY + [$FAILED] #set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($action['provider'])) #if None is not $provider #set $prov_list += ['%s'\ % (('', ' class="fail"')[$FAILED == $curStatus], $sbRoot, $provider.image_name(), $provider.name, ('%s%s' % ($order, 'th' if $order in [11, 12, 13] or str($order)[-1] not in $ordinal_indicators else $ordinal_indicators[str($order)[-1]]), 'Snatch failed')[$FAILED == $curStatus], $provider.name, $basename)] #set $order += (0, 1)[$curStatus in $SNATCHED_ANY] #else #set $prov_list += ['missing provider'\ % $sbRoot] #end if #end if #if $curStatus in [$DOWNLOADED, $ARCHIVED] #set $match = $re.search('\-(\w+)\.\w{3}\Z', $basename) #set $non_scene_note = '' #if not $match ## This fallback is for when idiots add a space and word to a release group. The space is converted ## to '\' which makes the regex parsing the scene group name fail, therefore we arrive here. ## A better solution would be to find where such data is parsed and saved to the db and perhaps ## fix at that point. But, in the meantime... #set $non_scene_resource = re.sub(r'(\-\w+)([\\]\w+)?(\.\w{3})\Z', r'\1\3', $action['resource']) #if $non_scene_resource #set $non_scene_note = ' (Non scene name: %s)' % $action['resource'].partition('-')[-1] #set $basename = $os.path.basename($non_scene_resource) #set $match = $re.search('\-(\w+)\.\w{3}\Z', $basename) #end if #end if #if $match #if $match.group(1).upper() in ('X264', '720P') #set $match = $re.search('(\w+)\-.*\-' + $match.group(1) + '\.\w{3}\Z', $os.path.basename($hItem['resource']), re.I) #end if #if $match #set $down_list += ['%s'\ % ($basename, $non_scene_note, $match.group(1).upper())] #end if #end if #end if #end for #if $sg_var('USE_SUBTITLES') #end if #end for ## ## #elif 'watched' in $layout ## ## #if not $results #else #for $hItem in $results #if $hItem.hide #continue #end if #set $compact = 'compact' in $layout and $hItem['rowid'] not in $mru_row_ids ## #set $curdatetime = $datetime.datetime.fromtimestamp($hItem.get('date_watched')) #set $curage = ($datetime.datetime.now() - $curdatetime).days #set $display_name = '%s - S%sE%s' % ( $hItem.get('data_name'), ('', ' class="grey-text"')[$hItem.get('deleted')], (('%s %s' % ($hItem.get('name1'), $hItem.get('name2'))), $hItem.get('show_name'))[$sg_var('SORT_ARTICLE') or not $hItem.get('name1')], $hItem.get('season'), $hItem.get('episode')) #end for #end if
Time Episode Action Provider Quality
 
$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)
$display_name#if $Quality.splitCompositeStatus($hItem['action'])[0] == $SNATCHED_PROPER then ' Proper' else ''# #if $DOWNLOADED == $curStatus #if '-1' != $hItem['provider'] $hItem['provider'] #end if #else #if 0 < $hItem['provider'] #if $curStatus in $SNATCHED_ANY + [$FAILED] #set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider'])) #if None is not $provider $provider.name #else Missing Provider #end if #else <%= hItem['provider'].capitalize() %> #end if #end if #end if $curQuality$Quality.get_quality_ui($curQuality)
Time Episode Snatched DownloadedSubtitledQuality
 
$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)
$display_name#if 'proper' in $hItem['resource'].lower or 'repack' in $hItem['resource'].lower then ' Proper' else ''# #echo ''.join($prov_list)# #echo ' '.join($down_list)# #for $action in reversed($hItem['actions']) #set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($action['action'])) #if $SUBTITLED == $curStatus $action['provider'] /   #end if #end for $Quality.get_quality_ui($curQuality)
Event DateEvent Age Played Episode Label (Profile) Quality Size Delete
0 Bytes #if $len($results) #end if

Media marked watched or unwatched will list in this space

$sbdatetime.sbdatetime.sbfdatetime($curdatetime)
${curage}d
#set $float_played = int($hItem.get('played'))/100.0 #set $value = ($float_played, int($float_played))[int($float_played) == $float_played] $value $display_name #set $label = re.sub('\{[^}]+\}$', '', $hItem.get('label')) #set $client = '' #try #set $client = re.findall('\{([^}]+)\}$', $hItem.get('label'))[0].lower() #set $client_label = ('%s %s' % ($client, $label)).strip(' ') $label #except $label #pass #end try $Quality.qualityStrings[$hItem.get('quality')].replace('SD DVD', 'SD DVD/BR/BD') $human($hItem.get('file_size')) #if $hItem.get('mru_count') #end if
#def row_class() #set global $row += 1 #echo ('even', 'odd')[bool($row % 2)] #end def #set global $row = 0 #set global $row = 0 ## ## #elif 'stats' in $layout ## ## #set sum = 0 #for $hItem in $stat_results #set $sum += $hItem['count'] #end for ## #if 'graph' in $layout ## ## #else ## ## #for $hItem in $stat_results #set $curdatetime = $datetime.datetime.strptime(str($hItem['latest']), $history.dateFormat) #end for #end if ## ## #elif 'failures' in $layout ## ##
#if not $provider_fails

No current failures. Failure stats display here when appropriate.

#else

When a provider cannot be contacted over a period, SickGear backs off and waits an increasing interval between each retry

#for $prov in $provider_fail_stats #if $len($prov['fails'])
#set $prov_class = '' #if not $prov['active'] #set $prov_class = $prov_class % 'class="grey-text" ' #else #set $prov_class = $prov_class % '' #end if $prov_class$prov['name'] #if $prov['active'] #if $prov['next_try'] #set nt = $str($prov['next_try']).split('.', 2)[0][::-1].replace(':', ' m', 1).replace(':', ' h', 1)[::-1] ... is paused until $sbdatetime.sbdatetime.sbftime($sbdatetime.sbdatetime.now() + $prov['next_try'], markup=True) (in ${nt}s) #end if #else ... is not enabled #end if
General help

Filters are saved per layout. Examples;

  • Event Date/Age, or Played: >7 and <60 (between 7d and 60d) , >1 (played more than once)
  • Label (Profile): emby or kodi , !kodi and !plex , emby user2 , emby user" (single end quote excludes user2)
  • Quality: sd or dl , blu

The above table is sorted first by played and then by date of event (i.e. watched/unwatched)

To multi-select checkboxes or column headers, click then hold shift and click

Key for Delete column;

  • Green Watched at least once at client
  • Red Partially watched or set 'unwatched' at client

To find how much freespace a delete will yield, the size tally increases for selected episodes that have a media file

A mapping in the client notification section is needed for results if a player library folder is different to a parent folder

In Compact layout, deleting records removes all episode related records. Detailed layout allows for individual selection [Show me]

Any script can add to the watched list by making a documented API call to sg.updatewatchedstate

Supported clients To use
Kodi

Isengard, Jarvis, Krypton
Episodes marked watched or unwatched are pushed in real-time and shown above.

Make the following changes at Kodi;

  1. Install the SickGear repo to access its Kodi Add-on
    • in Filemanager, add a source with <ip>:<port>/kodi/ (e.g. http://192.168.0.10:$sg_port/kodi/)
      and name it for example, SickGear. You will need to allow Unknown Sources if not already
    • in System/Add-ons, "Install from zip file", in the folder list, select the SickGear source
    • select the repository.sickgear in the folder listing, and install the repository zip
      Kodi will connect to the SickGear app to download and install its Add-on repository
  2. Install the SickGear Add-on from the repo
    • in System/Add-ons, "Install from zip repository", select "SickGear Add-on repository" / "Services"
    • select Add-on "SickGear Watched State Updater"
    • configure Add-on and restart Kodi after install or after switching profiles for the first time
Emby

Episode watch states are periodically fetched and shown above.

  1. Enable Emby Media Server in config/Notifications
  2. Choose an interval for updating watched states
Plex

Episode watch states are periodically fetched and shown above.

  1. Enable Plex Media Server in config/Notifications
  2. Choose an interval for updating watched states
#set $labels = [] #set $perc = [] #for $hItem in $stat_results #set $p = (float($hItem['count']) / float($sum)) * 100 #if 1 <= $p: #set $labels += [$hItem['provider']] #set $perc += ['%s' % re.sub(r'(\d+)(\.\d)\d+', r'\1\2', str($p))] #end if #end for
Provider Activity Hits Activity % Latest Activity
 
#set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider'])) #if None is not $provider $provider.name #else Missing Provider #end if $hItem['count'] #echo '%s%%' % re.sub(r'(\d+)(\.\d)\d+', r'\1\2', str((float($hItem['count'])/float($sum))*100))#
$sbdatetime.sbdatetime.sbfdatetime($curdatetime)
#if $prov['has_limit'] #end if #set $day = [] #for $fail in $prov['fails'] #set $child = True #if $fail['date'] not in $day #set $day += [$fail['date']] #set $child = False #end if #slurp# #if $fail['multirow'] #if not $child #else #end if #else #end if #set $blank = '-' #set $title=None #if $fail['http']['count'] #set $title=$fail['http']['code'] #end if #if $prov['has_limit'] #end if #end for
period of 1hr server/timeout network no data otherhit limit
$sbdatetime.sbdatetime.sbfdate($fail['date_time'])$sbdatetime.sbdatetime.sbftime($fail['date_time'], markup=True)$sbdatetime.sbdatetime.sbfdatetime($fail['date_time'], markup=True)#if $fail['http']['count']#$fail['http']['count']#else#$blank#end if# / #echo $fail['timeout'].get('count', 0) or $blank# #echo ($fail['connection'].get('count', 0) + $fail['connection_timeout'].get('count', 0)) or $blank# #echo $fail['nodata'].get('count', 0) or $blank# #echo $fail['other'].get('count', 0) or $blank##echo $fail.get('limit', {}).get('count', 0) or $blank#
#end if #end for #end if ## ## #end if #if 'failure' not in $layout #end if #include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_bottom.tmpl')