#import datetime #import re ## #import sickgear #from sickgear import history, providers, WEB_PORT #from sickgear.common import Quality, statusStrings, SNATCHED_ANY, SNATCHED_PROPER, DOWNLOADED, SUBTITLED, ARCHIVED, FAILED #from sickgear.helpers import human #from sickgear.providers import generic #from sickgear.sgdatetime import SGDatetime <% def sg_var(varname, default=False): return getattr(sickgear, varname, default) %>#slurp# <% def sg_str(varname, default=''): return getattr(sickgear, varname, default) %>#slurp# ## #import humanize ## #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 'Connect 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') and $earliest
Stats range from $SGDatetime.sbfdatetime($datetime.datetime.strptime(str($earliest), $history.dateFormat)) until $SGDatetime.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.split_composite_status(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.split_composite_status(int($action['action'])) #set $basename = $os.path.basename($action['resource']) #if $curStatus in $SNATCHED_ANY + [$FAILED] #set $provider = $providers.get_by_id($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(r'\-(\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(r'\-(\w+)\.\w{3}\Z', $basename) #end if #end if #if $match #if $match.group(1).upper() in ('X264', '720P') #set $match = $re.search(r'(\w+)\-.*\-' + $match.group(1) + r'\.\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
 
$SGDatetime.sbfdatetime($curdatetime, show_seconds=True)
$display_name#if $Quality.split_composite_status($hItem['action'])[0] == $SNATCHED_PROPER then ' Proper' else ''# #if $DOWNLOADED == $curStatus #if '-1' != $hItem['provider'] $hItem['provider'] #end if #else #if '-1' != $hItem['provider'] and len($hItem['provider']) #if $curStatus in $SNATCHED_ANY + [$FAILED] #set $provider = $providers.get_by_id($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
 
$SGDatetime.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.split_composite_status(int($action['action'])) #if $SUBTITLED == $curStatus $action['provider'] /   #end if #end for $Quality.get_quality_ui($curQuality)
Event DateEvent Age Played Label (Profile) Episode Quality Size Delete
0 Bytes #if $len($results) #end if

Media marked watched or unwatched will list in this space

$SGDatetime.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 #set $label = re.sub(r'\{[^}]+\}$', '', $hItem.get('label')) #set $client = '' #try #set $client = re.findall(r'\{([^}]+)\}$', $hItem.get('label'))[0].lower() #set $client_label = ('%s %s' % ($client, $label)).strip(' ') $label #except $label #pass #end try $display_name $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 ## ##
#for ($check, $check_name, $check_url) in [ ('tvdb', 'TVDB Api', 'api.thetvdb.com'), ('thexem', 'The Xem', 'thexem.info'), ('github', 'GitHub', 'github.com'), ]
Test if site is up
#end for
#if $sickgear.USE_NZBS
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
  • Size: <0 (deleted media) >321000000 (files greater than byte size)

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
Matrix and newer builds

Episodes marked watched or unwatched are pushed in real-time and shown above.

Oct 2022: Replace "/kodi/" with "/kodi-legacy/" in the following guide for Leia, Krypton, Jarvis, or Isengard


In Kodi media player;

  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/ or
      for SSL https://192.168.0.10:$sg_port/kodi/ ... see SSL guide) and name it for example, SickGear
      - You must allow Unknown Sources in Kodi settings if not already
    • in System/Add-ons, "Install from zip file", select the SickGear source
    • select repository.sickgear from the list, 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.get_by_id($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))#
$SGDatetime.sbfdatetime($curdatetime)
#set global $row = 0 #for $cur_provider in $sorted($sickgear.newznab_providers, key=lambda x: x.last_recent_search or SGDatetime(2000,1,1), reverse=True) #set $last_rls_date = '-' #set $last_rls_age = None #set $last_rls_age_str = '-' #if $cur_provider.last_recent_search #set $last_rls_date = $SGDatetime.sbfdatetime($cur_provider.last_recent_search) #set $last_rls_timedelta = ($SGDatetime.now() - $cur_provider.last_recent_search) #set $last_rls_age = $last_rls_timedelta.days #set $last_rls_age_str = '%s days ago' % $last_rls_age #set $tmp = $humanize.naturalday($cur_provider.last_recent_search, format='') #if $tmp #set $last_rls_age_str = $humanize.naturaltime($last_rls_timedelta) #if 'yesterday' == $tmp #set $is_now_night = 6 > $SGDatetime.now().hour #set $is_last_evening = 18 <= $cur_provider.last_recent_search.hour #if not ($is_now_night and $is_last_evening) #set $last_rls_age_str = $humanize.naturalday($cur_provider.last_recent_search) #end if #end if #end if #end if #set $status_class = 'yellow-bg' #set $status_text = 'not enabled' #if $cur_provider.is_active() #if not $cur_provider.enable_recentsearch #set $status_text = 'recent ' + $status_text #else #set $status_class = 'contrast-text green-bg' #set $status_text = 'enabled' #if '-' != $last_rls_date #if 4 > $last_rls_age #set $status_text = 'normal' #else #set $status_class = 'contrast-text' #if 14 >= $last_rls_age #set $status_class += ' blue-bg' #set $status_text = 'overdue' #else #set $status_class += ' red-bg' #set $status_text = 'not normal' #end if #end if #end if #end if #end if #end for
last release age dated provider is...
$cur_provider.name $last_rls_age_str $last_rls_date $status_text
#end if #if $domain_fail_cnt or $provider_fail_cnt #set $dev = (1, 3)[False] #set $last_n = 0 #set $show_bubble = 1 < $domain_fail_cnt or 1 < $provider_fail_cnt #if $show_bubble:
Bubble links:
#if 1 < $domain_fail_cnt #for $n, $item in enumerate($domain_fail_stats * $dev) #if $len($item['fails']) #set $last_n = $n + 1 #end if #end for #end if #if 1 < $provider_fail_cnt #for $n, $prov in enumerate($provider_fail_stats * $dev) #if $len($prov['fails']) #set $n += $last_n #end if #end for #end if
#end if

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

#end if #if not $domain_fail_cnt

No current domain failures. Failure stats display here when appropriate.

#else #for $n, $item in enumerate($domain_fail_stats * $dev) #if $len($item['fails'])
#set $item_class = '' #set $item_class = $item_class % '' #if $item['img']##elif $item['cls']##end if#$item_class$item['name'] #if $item['next_try'] #set nt = $str($item['next_try']).split('.', 2)[0][::-1].replace(':', ' m', 1).replace(':', ' h', 1)[::-1] ... is paused until $SGDatetime.sbftime($SGDatetime.now() + $item['next_try'], markup=True) (in ${nt}s) #end if
#if $item['has_limit'] #end if #set $day = [] #for $fail in $item['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 $item['has_limit'] #end if #end for
period of 1hr server/timeout network no data otherhit limit
$SGDatetime.sbfdate($fail['date_time'])$SGDatetime.sbftime($fail['date_time'], markup=True)$SGDatetime.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 #if not $provider_fail_cnt

No current provider failures. Failure stats display here when appropriate.

#else #for $n, $prov in enumerate($provider_fail_stats * $dev) #if $len($prov['fails'])
#set $n += $last_n
#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 $SGDatetime.sbftime($SGDatetime.now() + $prov['next_try'], markup=True) (in ${nt}s) #end if #else ... is not enabled #end if
#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
$SGDatetime.sbfdate($fail['date_time'])$SGDatetime.sbftime($fail['date_time'], markup=True)$SGDatetime.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')