Add choose to delete watched episodes from a list of played media at Kodi, Emby, and/or Plex.

Add episode watched state system that integrates with Kodi, Plex, and/or Emby, instructions at Shows/History/Layout/"Watched".
Add installable SickGear Kodi repository containing addon "SickGear Watched State Updater".
Change add Emby setting for watched state scheduler at Config/Notifications/Emby/"Update watched interval".
Change add Plex setting for watched state scheduler at Config/Notifications/Plex/"Update watched interval".
Add API cmd=sg.updatewatchedstate, instructions for use are linked to in layout "Watched" at /history.
Change history page table filter input values are saved across page refreshes.
Change history page table filter inputs, accept values like "dvd or web" to only display both.
Change history page table filter inputs, press 'ESC' key inside a filter input to reset it.
Add provider activity stats to Shows/History/Layout/ drop down.
Change move provider failures table from Manage/Media Search to Shows/History/Layout/Provider fails.
Change sort provider failures by most recent failure, and with paused providers at the top.
Change remove table form non-testing version 20007, that was reassigned.
This commit is contained in:
JackDandy 2018-03-06 01:18:08 +00:00
parent 04c129be4f
commit 8d9406d5fc
50 changed files with 3491 additions and 653 deletions

View file

@ -1,5 +1,19 @@
### 0.15.0 (2018-xx-xx xx:xx:xx UTC)
* Add choice to delete watched episodes from a list of played media at Kodi, Emby, and/or Plex,
instructions at Shows/History/Layout/"Watched"
* Add installable SickGear Kodi repository containing addon "SickGear Watched State Updater"
* Change add Emby setting for watched state scheduler at Config/Notifications/Emby/"Update watched interval"
* Change add Plex setting for watched state scheduler at Config/Notifications/Plex/"Update watched interval"
* Add API cmd=sg.updatewatchedstate, instructions for use are linked to in layout "Watched" at /history
* Change history page table filter input values are saved across page refreshes
* Change history page table filter inputs, accept values like "dvd or web" to only display both
* Change history page table filter inputs, press 'ESC' key inside a filter input to reset it
* Add provider activity stats to Shows/History/Layout/ drop down
* Change move provider failures table from Manage/Media Search to Shows/History/Layout/Provider fails
* Change sort provider failures by most recent failure, and with paused providers at the top
* Add SickGear-NZBGet dedicated post processing script, see.. \autoProcessTV\SickGear-NG\INSTALL.txt
* Add non standard multi episode name parsing e.g. S01E02and03 and 1x02and03and04
* Change overhaul and add API functions
* Change API version... start with 10
* Change set application response header to 'SickGear' + add API version
@ -90,8 +104,6 @@
* Add X-Filename response header to getbanner, getposter
* Add X-Fanartname response header for sg.show.getfanart
* Change remove some non-release group stuff from newnab results
* Add SickGear-NZBGet dedicated post processing script, see.. \autoProcessTV\SickGear-NG\INSTALL.txt
* Add non standard multi episode name parsing e.g. S01E02and03 and 1x02and03and04
[develop changelog]

View file

@ -653,22 +653,32 @@ inc_bottom.tmpl
display:inline
}
#history-table thead th .icon-glyph,
.footer .icon-glyph{
opacity:0.4;filter:alpha(opacity=40);
float:none;
display:inline-block;
margin:0 0 -1px 2px;
height:12px;
margin:0 0 -2px 4px;
height:13px;
width:14px
}
#history-table thead th .icon-glyph:hover,
#history-table tfoot th .icon-glyph:hover,
.footer .icon-glyph:hover{
opacity:0.6;filter:alpha(opacity=60)
}
#history-table thead th .icon-glyph:hover,
.footer .icon-glyph:hover{
opacity:0.6;filter:alpha(opacity=60);
cursor:pointer
}
#history-table thead th .icon-glyph.age,
#history-table thead th .icon-glyph.date:hover,
.footer .icon-glyph.timeleft,
.footer .icon-glyph.time:hover{
background-position:-49px -25px
}
#history-table thead th .icon-glyph.date,
#history-table thead th .icon-glyph.age:hover,
.footer .icon-glyph.time,
.footer .icon-glyph.timeleft:hover{
background-position:-193px -121px
@ -2605,40 +2615,51 @@ h2.day.add-apace, h2.network.add-space{
/* =======================================================================
history.tmpl
========================================================================== */
.strike-deleted{
text-decoration:line-through
}
#historyTable td,
#historyTable td.provider span{
.red-bg{
background-color:#992828
}
.green-bg{
background-color:#3f993f
}
#history-table td,
#history-table td.provider span{
text-align:center
}
#historyTable td.provider span{
#history-table td.provider span{
padding:0 2px
}
#historyTable td.provider > img{
#history-table td.provider > img{
margin-right:3px
}
#historyTable td.provider span.fail img{
#history-table td.provider span.fail img{
opacity:0.5;
filter:alpha(opacity=50)
}
#historyTable td img,
#historyTable td span{
#history-table td img,
#history-table td span{
vertical-align:middle
}
#historyTable td span.article{
#history-table td span.article{
vertical-align:initial
}
#historyTable td img.help,
#historyTable td span.help{
#history-table td img.help,
#history-table td span.help{
cursor:help
}
#historyTable td.tvShow{
#history-table td.tvShow{
text-align:left
}
@ -3256,15 +3277,18 @@ input.get_less_eps{
#media-search .section{
padding-bottom:10px
}
#media-search .btn{
#media-search .btn,
#provider-failures .btn{
margin:0 6px 0 0;
min-width:70px
}
#media-search .btn.shows-more,
#media-search .btn.shows-less{
#media-search .btn.shows-less,
#provider-failures .btn.shows-more,
#provider-failures .btn.shows-less{
margin:6px 6px 6px 0;
}
#media-search .btn.provider-retry{
#provider-failures .btn.provider-retry{
margin:6px 0 6px 4px;
}
.tablesorter.provider-failures{width:auto;clear:both;margin-bottom:10px}
@ -3356,6 +3380,16 @@ input.get_less_eps{
Global
========================================================================== */
.contrast-text,
a.contrast-text:focus,
a.contrast-text:active,
a.contrast-text:visited{
color:#ddd
}
a.contrast-text:hover{
color:#999
}
span.path{
padding:3px;
margin-left:3px

View file

@ -4,6 +4,8 @@
#from lib.libtrakt import TraktAPI
#from sickbeard.helpers import anon_url, starify
#from sickbeard.notifiers import NotifierFactory
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
##
#set global $title = 'Config - Notifications'
#set global $header = 'Notifications'
@ -91,6 +93,21 @@
</span>
</label>
</div>
#set $selected = ' selected="selected" class="selected"'
<div class="field-pair">
<label for="emby-watched-interval">
<span class="component-title">Update watched interval</span>
<span class="component-desc">
<select id="emby-watched-interval" name="emby_watched_interval" class="form-control input-sm">
<option value="0"#if not $sg_var('EMBY_WATCHEDSTATE_SCHEDULED')#${selected}#end if#>Off </option>
#for v in [10, 15, 30, 45, 60]
<option value="$v"#if $sg_var('EMBY_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('EMBY_WATCHEDSTATE_FREQUENCY')#${selected}#end if#>$v #if not $sg_var('EMBY_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('EMBY_WATCHEDSTATE_FREQUENCY')#(recent) #end if#</option>
#end for
</select>
<span>minutes to fetch episode watched states for the history page</span>
</span>
</label>
</div>
<div class="field-pair">
<label for="emby-host">
<span class="component-title">Host(s) running Emby</span>
@ -285,30 +302,43 @@
<label for="plex-update-library">
<span class="component-title">Update server library</span>
<span class="component-desc">
<input type="checkbox" class="enabler" name="plex_update_library" id="plex-update-library" #if $sickbeard.PLEX_UPDATE_LIBRARY then 'checked="checked" ' else ''#/>
<input type="checkbox" name="plex_update_library" id="plex-update-library" #if $sickbeard.PLEX_UPDATE_LIBRARY then 'checked="checked" ' else ''#/>
<p>update Plex Media Server library when a download finishes</p>
</span>
</label>
</div>
<div id="content_plex-update-library">
<div class="field-pair">
<label for="plex-server-host">
<span class="component-title">Plex Media Server IP:Port</span>
<span class="component-desc">
<input type="text" name="plex_server_host" id="plex-server-host" value="<%= re.sub(r'\b,\b', ', ', sickbeard.PLEX_SERVER_HOST) %>" class="form-control input-sm input350">
<div class="clear-left">
<p>one or more hosts running Plex Media Server<br>(eg. 192.168.1.1:32400, 192.168.1.2:32400)</p>
</div>
</span>
</label>
</div>
#set $selected = ' selected="selected" class="selected"'
<div class="field-pair">
<label for="plex-watched-interval">
<span class="component-title">Update watched interval</span>
<span class="component-desc">
<select id="plex-watched-interval" name="plex_watched_interval" class="form-control input-sm">
<option value="0"#if not $sg_var('PLEX_WATCHEDSTATE_SCHEDULED')#${selected}#end if#>Off </option>
#for v in [10, 15, 30, 45, 60]
<option value="$v"#if $sg_var('PLEX_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('PLEX_WATCHEDSTATE_FREQUENCY')#${selected}#end if#>$v #if not $sg_var('PLEX_WATCHEDSTATE_SCHEDULED') and $v == $sg_var('PLEX_WATCHEDSTATE_FREQUENCY')#(recent) #end if#</option>
#end for
</select>
<span>minutes to fetch episode watched states for the history page</span>
</span>
</label>
</div>
<div class="field-pair">
<label for="plex-server-host">
<span class="component-title">Plex Media Server IP:Port</span>
<span class="component-desc">
<input type="text" name="plex_server_host" id="plex-server-host" value="<%= re.sub(r'\b,\b', ', ', sickbeard.PLEX_SERVER_HOST) %>" class="form-control input-sm input350">
<div class="clear-left">
<p>one or more hosts running Plex Media Server<br>(eg. 192.168.1.1:32400, 192.168.1.2:32400)</p>
</div>
</span>
</label>
</div>
<div class="field-pair">
<div class="test-notification" id="test-pms-result">Click below to test Plex server(s)</div>
<input class="btn" type="button" value="Test Plex Server" id="test-pms">
<input type="submit" class="config_submitter btn" value="Save Changes">
<div class="clear-left">&nbsp;</div>
</div>
<div class="field-pair">
<div class="test-notification" id="test-pms-result">Click below to test Plex server(s)</div>
<input class="btn" type="button" value="Test Plex Server" id="test-pms">
<input type="submit" class="config_submitter btn" value="Save Changes">
<div class="clear-left">&nbsp;</div>
</div>
</div>

View file

@ -114,7 +114,7 @@
<span class="component-title">Extra scripts</span>
<span class="component-desc">
<input type="text" name="extra_scripts" id="extra_scripts" value="<%='|'.join(sickbeard.EXTRA_SCRIPTS)%>" class="form-control input-sm input350">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_extra_params" title="Toggle info for script arguments" class="legend" class="legend" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_extra_params" title="Toggle info for script arguments">
<div class="clear-left">
<p class="note">scripts are called after built-in post processing.
&nbsp;<b>note:</b> use <b class="grey-text boldest">|</b> to separate additional extra scripts
@ -332,7 +332,7 @@
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_pattern" id="naming_pattern" value="$sickbeard.NAMING_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_key" title="Toggle Naming Legend" class="legend" class="legend" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_key" title="Toggle Naming Legend">
</span>
</label>
</div>
@ -552,7 +552,7 @@
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_abd_pattern" id="naming_abd_pattern" value="$sickbeard.NAMING_ABD_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_abd_key" title="Toggle ABD Naming Legend" class="legend" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_abd_key" title="Toggle ABD Naming Legend">
</span>
</label>
</div>
@ -750,7 +750,7 @@
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_sports_pattern" id="naming_sports_pattern" value="$sickbeard.NAMING_SPORTS_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_sports_key" title="Toggle Sports Naming Legend" class="legend" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_sports_key" title="Toggle Sports Naming Legend">
</span>
</label>
</div>
@ -953,7 +953,7 @@
<span class="component-title"></span>
<span class="component-desc">
<input type="text" name="naming_anime_pattern" id="naming_anime_pattern" value="$sickbeard.NAMING_ANIME_PATTERN" class="form-control input-sm input350 custom-pattern">
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_anime_key" title="Toggle Anime Naming Legend" class="legend" />
<img src="$sbRoot/images/legend16.png" width="16" height="16" alt="[Toggle Key]" id="show_naming_anime_key" title="Toggle Anime Naming Legend">
</span>
</label>
</div>
@ -1264,4 +1264,4 @@
//-->
</script>
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -1,64 +1,41 @@
#import datetime
#import re
##
#import sickbeard
#from sickbeard import history, providers, sbdatetime
#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 global $title = 'History'
#set global $header = 'History'
#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 <span class="grey-text">: %s</span>' % $layout_name
#set global $sbPath = '..'
#set global $topmenu = 'home'
#set $layout = $sg_str('HISTORY_LAYOUT', 'detailed')
##
#import os.path
#include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_top.tmpl')
##
#set $checked = ' checked="checked"'
<script type="text/javascript">
<script src="$sbRoot/js/history.js?v=$sbPID"></script>
<script>
<!--
\$.tablesorter.addParser({
id: 'cDate',
is: function(s) {
return false;
},
format: function(s) {
return s;
},
type: 'numeric'
});
\$.SickGear.history = {
layoutName: '$layout',
isCompact: #echo ('!1', '!0')['compact' in $layout]#,
isTrashit: #echo ('!1', '!0')[bool($sg_var('TRASH_REMOVE_SHOW'))]#,
useSubtitles: #echo ('!1', '!0')[bool($sg_var('USE_SUBTITLES'))]#,
lastDeleteFiles: '#echo ('', $checked)[$getVar('last_delete_files', False)]#',
lastDeleteRecords: '#echo ('', $checked)[$getVar('last_delete_records', False)]#',
};
\$(document).ready(function()
{
\$('#historyTable:has(tbody tr)').tablesorter({
widgets: ['zebra', 'filter'],
sortList: [[0, 1]],
textExtraction: {
0: function(node) { return \$(node).find('span').text().toLowerCase(); },
#if ('detailed' == $layout)
4: function(node) { return \$(node).find('span').text().toLowerCase(); }
#else
1: function(node) { return \$(node).find('span[data-name]').attr('data-name').toLowerCase(); },
2: function(node) { return \$(node).attr('provider').toLowerCase(); },
5: function(node) { return \$(node).attr('quality').toLowerCase(); }
#end if
},
headers: {
0: { sorter: 'cDate' },
#if ('detailed' == $layout)
4: { sorter: 'quality' }
#else
4: { sorter: false },
5: { sorter: 'quality' }
#end if
}
});
\$('#limit').change(function(){
window.location.href = '$sbRoot/history/?limit=' + \$(this).val()
});
\$(document).ready(function() {
#set $fuzzydate = 'airdate'
#if $sg_var('FUZZY_DATING')
@ -80,31 +57,64 @@
#else
<h1 class="title">$title</h1>
#end if
#if $varExists('earliest')
<div class="grey-text" style="clear:both;margin:-8px 0 1.5em 2px;font-size:0.85em;float:left">
Stats range from <span class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($datetime.datetime.strptime(str($earliest), $history.dateFormat))</span> until <span class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($datetime.datetime.strptime(str($latest), $history.dateFormat))</span>
</div>
#end if
#set $html_selected = ' selected="selected"'
#set $selected = ' selected="selected" class="selected"'
##
<div class="h2footer pull-right">Limit:
<div id="results-sortby" class="h2footer pull-right">Layout:
<select name="limit" id="limit" class="form-control form-control-inline input-sm">
<option value="100"#echo ('', $html_selected)['100' == $limit]#>100</option>
<option value="250"#echo ('', $html_selected)['250' == $limit]#>250</option>
<option value="500"#echo ('', $html_selected)['500' == $limit]#>500</option>
<option value="0"#echo ('', $html_selected)['0' == $limit]#>All</option>
<option value="100"#echo ('', $selected)['100' == $limit]#>100</option>
<option value="250"#echo ('', $selected)['250' == $limit]#>250</option>
<option value="500"#echo ('', $selected)['500' == $limit]#>500</option>
<option value="0"#echo ('', $selected)['0' == $limit]#>All</option>
</select>
<span style="margin-left:5px">Layout:
<span style="margin-left:5px">
<select name="HistoryLayout" class="form-control form-control-inline input-sm" onchange="location = this.options[this.selectedIndex].value">
<option value="$sbRoot/setHistoryLayout/?layout=compact"#echo ('', $html_selected)['compact' == $sg_str('HISTORY_LAYOUT')]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed"#echo ('', $html_selected)['detailed' == $sg_str('HISTORY_LAYOUT', 'detailed')]#>Detailed</option>
<optgroup label="Activity">
<option value="$sbRoot/setHistoryLayout/?layout=compact"#echo ('', $selected)['compact' == $sg_str('HISTORY_LAYOUT')]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed"#echo ('', $selected)['detailed' == $sg_str('HISTORY_LAYOUT', 'detailed')]#>Detailed</option>
</optgroup>
<optgroup label="Watched">
<option value="$sbRoot/setHistoryLayout/?layout=compact_watched"#echo ('', $selected)['compact_watched' == $sg_str('HISTORY_LAYOUT')]#>Compact</option>
<option value="$sbRoot/setHistoryLayout/?layout=detailed_watched"#echo ('', $selected)['detailed_watched' == $sg_str('HISTORY_LAYOUT')]#>Detailed</option>
</optgroup>
<optgroup label="Stats">
<option value="$sbRoot/setHistoryLayout/?layout=compact_stats"#echo ('', $selected)['compact_stats' == $sg_str('HISTORY_LAYOUT')]#>Activity hits</option>
<option value="$sbRoot/setHistoryLayout/?layout=graph_stats"#echo ('', $selected)['graph_stats' == $sg_str('HISTORY_LAYOUT')]#>Graphed hits</option>
<option value="$sbRoot/setHistoryLayout/?layout=provider_failures"#echo ('', $selected)['provider_failures' == $sg_str('HISTORY_LAYOUT')]#>Provider fails</option>
</optgroup>
</select>
</span>
</div>
<br>
<table id="historyTable" class="sickbeardTable tablesorter $layout" cellspacing="1" border="0" cellpadding="0">
<style>
#watched-help thead tr th, #watched-help tbody tr td{text-align:left}
#watched-help tbody td ol, #watched-help tbody td ul, #watched-help tbody td p{margin-bottom:0}
#watched-help tbody td img{margin-right:3px}
#watched-help .vmid{vertical-align:middle}
#history-table .age{display:none}
#history-table.event-age .age{display:inline-block}
#history-table.event-age .date{display:none}
</style>
##
#if 'failure' not in $layout
##
<table id="history-table" data-table-group="$layout" class="sickbeardTable tablesorter $layout" cellspacing="1" border="0" cellpadding="0">
##
#end if
##
##
#if 'detailed' == $layout
##
##
<thead>
<tr>
<th class="nowrap">Time</th>
<th class="text-nowrap">Time</th>
<th width="35%">Episode</th>
<th>Action</th>
<th>Provider</th>
@ -114,21 +124,20 @@
<tfoot>
<tr>
<th class="nowrap" colspan="5">&nbsp;</th>
<th class="text-nowrap" colspan="5">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $historyResults
#for $hItem in $history_results
#set $curStatus, $curQuality = $Quality.splitCompositeStatus(int($hItem['action']))
#set $data_name = (re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')]
#set $display_name = '<span data-name="%s">%s - S%02iE%02i</span>' % (
$data_name,
(re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'<span class="article">\1</span> \2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')],
int(hItem['season']), int(hItem['episode']))
#set $display_name = '<span data-sort="%s">%s - S%02iE%02i</span>' % (
$hItem['data_name'],
(('<span class="article">%s</span> %s' % ($hItem['name1'], $hItem['name2'])), $hItem['show_name'])[$sg_var('SORT_ARTICLE') or not $hItem['name1']],
int($hItem['season']), int($hItem['episode']))
<tr>
#set $curdatetime = $datetime.datetime.strptime(str($hItem['date']), $history.dateFormat)
<td><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort-data">$time.mktime($curdatetime.timetuple())</span></td>
<td><div class="${fuzzydate}" data-sort="$time.mktime($curdatetime.timetuple())">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div></td>
<td class="tvShow"><a href="$sbRoot/home/displayShow?show=$hItem['showid']#season-$hItem['season']">$display_name#if $Quality.splitCompositeStatus($hItem['action'])[0] == $SNATCHED_PROPER then ' <span class="quality Proper">Proper</span>' else ''#</a></td>
<td#echo ('', ' class="subtitles_column"')[$SUBTITLED == $curStatus]#>
#if $SUBTITLED == $curStatus
@ -159,11 +168,14 @@
<td><span class="hide">$curQuality</span><span class="quality $Quality.get_quality_css($curQuality)">$Quality.get_quality_ui($curQuality)</span></td>
</tr>
#end for
#else
##
##
#elif ('compact' == $layout)
##
##
<thead>
<tr>
<th class="nowrap">Time</th>
<th class="text-nowrap">Time</th>
<th width="#echo '3%s%%' % ('5', '0')[$sg_var('USE_SUBTITLES')]#">Episode</th>
<th>Snatched</th>
<th>Downloaded</th>
@ -176,18 +188,17 @@
<tfoot>
<tr>
<th class="nowrap" colspan="6">&nbsp;</th>
<th class="text-nowrap" colspan="6">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $compactResults
#for $hItem in $compact_results
#set $curdatetime = $datetime.datetime.strptime(str($hItem['actions'][0]['time']), $history.dateFormat)
#set $data_name = (re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'\2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')]
#set $display_name = '<span data-name="%s">%s - S%02iE%02i</span>' % (
$data_name,
(re.sub('^((?:A(?!\s+to)n?)|The)\s(\w)', r'<span class="article">\1</span> \2', $hItem['show_name']), $hItem['show_name'])[$sg_var('SORT_ARTICLE')],
int(hItem['season']), int(hItem['episode']))
#set $display_name = '<span data-sort="%s">%s - S%02iE%02i</span>' % (
$hItem['data_name'],
(('<span class="article">%s</span> %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
@ -204,8 +215,8 @@
$provider.name, $basename)]
#set $order += (0, 1)[$curStatus in $SNATCHED_ANY]
#else
#set $prov_list += ['<img src="%s/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider" />'\
% $sbRoot]
#set $prov_list += ['<img src="%s/images/providers/missing.png" width="16" height="16" alt="missing provider" title="missing provider" />'\
% $sbRoot]
#end if
#end if
#if $curStatus in [$DOWNLOADED, $ARCHIVED]
@ -235,7 +246,7 @@
#end if
#end for
<tr>
<td><div class="${fuzzydate}">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div><span class="sort-data">$time.mktime($curdatetime.timetuple())</span></td>
<td><div class="${fuzzydate}" data-sort="$time.mktime($curdatetime.timetuple())">$sbdatetime.sbdatetime.sbfdatetime($curdatetime, show_seconds=True)</div></td>
<td class="tvShow">
<span><a href="$sbRoot/home/displayShow?show=$hItem['show_id']#season-$hItem['season']">$display_name#if 'proper' in $hItem['resource'].lower or 'repack' in $hItem['resource'].lower then ' <span class="quality Proper">Proper</span>' else ''#</a></span>
</td>
@ -261,9 +272,403 @@
<td quality="$curQuality"><span class="quality $Quality.get_quality_css($curQuality)">$Quality.get_quality_ui($curQuality)</span></td>
</tr>
#end for
##
##
#elif 'watched' in $layout
##
##
<thead>
<tr>
<th><span class="date">Event Date</span><span class="age">Event Age</span></th>
<th width="8%">Played</th>
<th class="text-nowrap">Episode</th>
<th class="text-nowrap" width="15%">Label (Profile)</th>
<th class="text-nowrap" width="10%">Quality</th>
<th class="text-nowrap" width="10%">Size</th>
<th width="10%">Delete</th>
</tr>
</thead>
#end if
<tfoot>
<tr>
<th>
<i style="background-image:url($sbRoot/images/legend16.png)" id="show-watched-help" title="Toggle help" class="add-qtip icon-glyph"></i>
<span id="row-count" style="font-size:12px;line-height:20px;float:left"></span>
</th>
<th colspan="4"></th>
<th><span id="sum-size" style="border-top:solid 1px #ddd">0 Bytes</span></th>
<th>
#if $len($results)
<input id="del-watched" type="button" class="btn" value="Submit">
#end if
</th>
</tr>
</tfoot>
<tbody id="tbody">
#if not $results
<tr colspan="7">
<td colspan="7" style="text-align:center">
<p>Media marked watched or unwatched will list in this space</p>
</td>
</tr>
#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 = '<span data-sort="%s"%s>%s - S%sE%s</span>' % (
$hItem.get('data_name'),
('', ' class="grey-text"')[$hItem.get('deleted')],
(('<span class="article">%s</span> %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'))
<tr data-tvep-id="$hItem['tvep_id']" data-file="$hItem['location']"#if $compact# class="hide"#end if#>
<td>
<div class="date ${fuzzydate}" data-sort="$curage">$sbdatetime.sbdatetime.sbfdatetime($curdatetime)</div>
<div class="age">${curage}d</div>
</td>
<td>
#set $float_played = int($hItem.get('played'))/100.0
#set $value = ($float_played, int($float_played))[int($float_played) == $float_played]
<span#if not $bool($int($value))# class="add-qtip" title="Marked Unwatched"#end if#>$value</span>
</td>
<td class="tvShow text-nowrap">
<span class="add-qtip#if not $hItem.get('deleted')#"#else# strike-deleted" title="file no longer exists"#end if#>
<a href="$sbRoot/home/displayShow?show=$hItem.get('showid')#season-$hItem.get('season')">$display_name</a>
</span>
</td>
<td>
#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(' ')
<img height="16px" style="margin-right:3px" src="$sbRoot/images/notifiers/${client}.png"><span data-sort="$client_label" style="vertical-align:middle">$label</span>
#except
<span data-sort="${label}">$label</span>
#pass
#end try
</td>
<td quality="$hItem.get('quality')" class="text-nowrap">
<span class="quality $Quality.get_quality_css($hItem.get('quality')) add-qtip" title="#if $hItem.get('deleted')#file no longer exists#else#$hItem['location']#end if#">$Quality.qualityStrings[$hItem.get('quality')].replace('SD DVD', 'SD DVD/BR/BD')</span>
</td>
<td class="size text-nowrap">
<span#if $hItem.get('deleted')# class="add-qtip grey-text strike-deleted" title="file no longer exists"#end if# data-sort="$hItem.get('file_size')">$human($hItem.get('file_size'))</span>
</td>
<td class="#echo ('green', 'red')[100 > $hItem.get('mru_count')]#-bg">
<input id="del-$hItem.get('rowid')-$hItem.get('indexer')-$hItem.get('showid')"
title="last&nbsp;event<br>#echo ('watched', 'unwatched')[100 > $hItem.get('mru_count')]#"
type="checkbox" class="del-check add-qtip">
#if $hItem.get('mru_count')
<span style="position:absolute">
<i class="icon-glyph" style="top:-6px;right:4px;position:relative; opacity: 0.4;background-position: -95px -119px"></i>
</span>
#end if
</td>
</tr>
#end for
#end if
</tbody>
</table>
#def row_class()
#set global $row += 1
#echo ('even', 'odd')[bool($row % 2)]
#end def
<table id="watched-help" style="#if $hide_watched_help#display:none;#end if#margin-top:15px" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0">
<thead>
<tr>
<th colspan="2">General help</th>
</tr>
</thead>
#set global $row = 0
<tbody>
<tr class="$row_class()">
<td colspan="2">
<p>Filters are saved per layout. Examples;</p>
<ul>
<li>Event Date/Age, or Played: <span class="grey-text">&gt;7 and &lt;60</span> (between 7d and 60d) , <span class="grey-text">&gt;1</span> (played more than once)</li>
<li>Label (Profile): <span class="grey-text">emby or kodi</span> , <span class="grey-text">!kodi and !plex</span> , <span class="grey-text">emby user2</span> , <span class="grey-text">emby user"</span> (single end quote excludes user2)</li>
<li>Quality: <span class="grey-text">sd or dl</span> , <span class="grey-text">blu</span></li>
</ul>
</td
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>The above table is sorted first by played and then by date of event (i.e. watched/unwatched)</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>To multi-select checkboxes or column headers, click then hold shift and click</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p style="margin-bottom:6px">Key for <span class="grey-text">Delete</span> column;</p>
<ul>
<li style="line-height:25px"><span class="contrast-text green-bg" style="padding:4px 18px 0 6px;margin:0 6px 0 0">Green
<span style="position:absolute">
<i class="icon-glyph" style="position:relative;top:-2px;right:8px;opacity: 0.4;background-position:-95px -119px"></i>
</span>
</span>Watched at least once at client</li>
<li style="line-height:25px"><span class="contrast-text red-bg" style="padding:4px 20px 0 6px;margin:0 6px 0 0">Red
</span>Partially watched or set 'unwatched' at client
</li>
</ul>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>To find how much freespace a delete will yield, the size tally increases for selected episodes that have a media file</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>In <span class="grey-text">Compact</span> layout, deleting records removes all episode related records. <span class="grey-text">Detailed</span> layout allows for individual selection [<a rel="dialog" href="https://raw.githubusercontent.com/wiki/SickGear/SickGear/images/screenies/watched.png">Show me</a>]</p>
</td>
</tr>
<tr class="$row_class()">
<td colspan="2">
<p>Any script can add to the watched list by making a <a href="https://github.com/SickGear/SickGear/wiki/API#sg.updatewatchedstate">documented API call</a> to <code>sg.updatewatchedstate</code></p>
</td>
</tr>
</tbody>
<thead>
<tr>
<th width="20%">Supported clients</th>
<th>To use</th>
</tr>
</thead>
#set global $row = 0
<tbody>
<tr class="$row_class()">
<td><img height="16px" src="$sbRoot/images/notifiers/kodi.png"><span class="vmid">Kodi</span>
<p><em class="grey-text">Isengard, Jarvis, Krypton</em><br>
Episodes marked watched or unwatched are pushed in real-time and shown above.</p>
</td>
<td>
<p>Make the following changes at Kodi;</p>
<ol>
<li>Install the SickGear repo to access its Kodi Add-on
<ul>
<li>in <b class="boldest">Filemanager</b>, add a source for SickGear with <span class="grey-text">&lt;ip&gt;:&lt;port&gt;/kodi/</span> (e.g. <span class="grey-text">192.168.0.10:$sg_port/kodi/</span>)<br>
and name it for example, <span class="grey-text">SickGear</span>. <em>You will need to allow <span class="highlight-text">Unknown Sources</span> if not already</em> </li>
<li>in <b class="boldest">System/Add-ons</b>, "<span class="grey-text">Install from zip file</span>", in the folder list, select the <span class="grey-text">SickGear</span> source</li>
<li>select the <span class="grey-text">repository.sickgear</span> in the folder listing, and install the repository zip<br>
<em>Kodi will connect to the SickGear app to download and install its Add-on repository</em></li>
</ul>
<li>Install the SickGear Add-on from the repo</li>
<ul>
<li>in <b class="boldest">System/Add-ons</b>, "<span class="grey-text">Install from zip repository</span>", select "<span class="grey-text">SickGear Add-on repository</span>" / "<span class="grey-text">Services</span>"<br>
<li>select Add-on "<span class="grey-text">SickGear Watched State Updater</span>"</li>
<li>configure Add-on and restart Kodi after install or after switching profiles for the first time</li>
</ul>
</ol>
</td>
</tr>
<tr class="$row_class()">
<td><img height="16px" src="$sbRoot/images/notifiers/emby.png"><span class="vmid">Emby</span>
<p>Episode watch states are periodically fetched and shown above.</p>
</td>
<td>
<ol>
<li>Enable Emby Media Server in <b class="boldest">config/Notifications</b></li>
<li>Choose an interval for updating watched states</li>
</ol>
</td>
</tr>
<tr class="$row_class()">
<td><img height="16px" src="$sbRoot/images/notifiers/plex.png"><span class="vmid">Plex</span>
<p>Episode watch states are periodically fetched and shown above.</p>
</td>
<td>
<ol>
<li>Enable Plex Media Server in <b class="boldest">config/Notifications</b></li>
<li>Choose an interval for updating watched states</li>
</ol>
</td>
</tr>
##
##
#elif 'stats' in $layout
##
##
#set sum = 0
#for $hItem in $stat_results
#set $sum += $hItem['count']
#end for
##
#if 'graph' in $layout
<tbody>
<tr><td>
#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
<script src="$sbRoot/js/plot.ly/plotly-latest.min.js?v=$sbPID"></script>
<script src="$sbRoot/js/plot.ly/numeric/1.2.6/numeric.min.js?v=$sbPID"></script>
<div id="plot-canvas" style="margin:15px auto 15px;width:550px;height:350px"></div>
<style>
.modebar-btn[data-title*="edit plot"]{display:none !important}
</style>
<script>
Plotly.newPlot('plot-canvas', [
{
values: [#echo ', '.join($perc)#],
labels: [#echo '\'%s\'' % '\', \''.join($labels)#],
name: 'SickGear provider activity',
hoverinfo: 'label+percent+name',
domain: {x: [0, .5]},
hole: .42,
type: 'pie'
}
],
{title: 'SickGear provider activity (1% and above for #echo ('latest %s' % $limit, 'all')['0' == $limit]#)',
annotations: [{font: {size: 14}, showarrow: false, text: 'Activity', x: 0.16, y: 0.5}]
}, {displaylogo: !1, showLink: !1});
</script>
</td></tr>
##
##
#else
##
##
<thead>
<tr>
<th>Provider</th>
<th>Activity Hits</th>
<th>Activity %</th>
<th>Latest Activity</th>
</tr>
</thead>
<tfoot>
<tr>
<th class="text-nowrap" colspan="4">&nbsp;</th>
</tr>
</tfoot>
<tbody>
#for $hItem in $stat_results
<tr>
<td class="provider text-nowrap">
#set $provider = $providers.getProviderClass($generic.GenericProvider.make_id($hItem['provider']))
#if None is not $provider
<img src="$sbRoot/images/providers/<%= provider.image_name() %>" width="16" height="16"><span data-sort="$hItem['provider']">$provider.name</span>
#else
<img src="$sbRoot/images/providers/missing.png" width="16" height="16" title="missing provider"><span data-sort="$hItem['provider']">Missing Provider</span>
#end if
</td>
<td>$hItem['count']</td>
<td>#echo '%s%%' % re.sub(r'(\d+)(\.\d)\d+', r'\1\2', str((float($hItem['count'])/float($sum))*100))#</td>
#set $curdatetime = $datetime.datetime.strptime(str($hItem['latest']), $history.dateFormat)
<td><div class="${fuzzydate}" data-sort="$hItem['latest']">$sbdatetime.sbdatetime.sbfdatetime($curdatetime)</div></td>
</tr>
#end for
#end if
##
##
#elif 'failures' in $layout
##
##
<div id="provider-failures">
#if not $provider_fails
<p>No current failures. Failure stats display here when appropriate.</p>
#else
<p>When a provider cannot be contacted over a period, SickGear backs off and waits an increasing interval between each retry</p>
#for $prov in $provider_fail_stats
#if $len($prov['fails'])
<!-- $prov['name'] -->
<div>
#set $prov_class = '<span %sstyle="vertical-align:middle">'
#if not $prov['active']
#set $prov_class = $prov_class % 'class="grey-text" '
#else
#set $prov_class = $prov_class % ''
#end if
<input type="button" class="shows-more btn" value="Expand" style="display:none"><input type="button" class="shows-less btn" value="Collapse"><img src="$sbRoot/images/providers/$prov['prov_img']" width="16" height="16" style="margin:0 6px 0 3px">$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) <input type="button" class="provider-retry btn" id="$prov['prov_id']-btn-retry" value="Ignore pause on next search">
#end if
#else
... is not enabled
#end if
</span>
</div>
<table class="manageTable provider-failures tablesorter hover-highlight focus-highlight text-center" cellspacing="0" border="0" cellpadding="0">
<thead>
<tr>
<th class="text-center" style="width:13em;padding-right:20px">period of 1hr</th>
<th class="text-center" style="padding-right:20px">server/timeout</th>
<th class="text-center" style="padding-right:20px">network</th>
<th class="text-center" style="padding-right:20px">no data</th>
<th class="text-center" style="padding-right:20px">other</th>
#if $prov['has_limit']
<th class="text-center" style="padding-right:20px">hit limit</th>
#end if
</tr>
</thead>
<tbody>
#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#
<tr#if $fail['multirow'] and $child# class="tablesorter-childRow"#end if#>
#if $fail['multirow']
#if not $child
<td><a href="#" class="provider-fail-parent-toggle" title="Totals (expand for detail)">$sbdatetime.sbdatetime.sbfdate($fail['date_time'])</a></td>
#else
<td>$sbdatetime.sbdatetime.sbftime($fail['date_time'], markup=True)</td>
#end if
#else
<td>$sbdatetime.sbdatetime.sbfdatetime($fail['date_time'], markup=True)</td>
#end if
#set $blank = '-'
#set $title=None
#if $fail['http']['count']
#set $title=$fail['http']['code']
#end if
<td>#if $fail['http']['count']#<span title="#if $child or not $fail['multirow']#$title#else#Expand for fail codes#end if#">$fail['http']['count']</span>#else#$blank#end if# / #echo $fail['timeout'].get('count', 0) or $blank#</td>
<td>#echo ($fail['connection'].get('count', 0) + $fail['connection_timeout'].get('count', 0)) or $blank#</td>
<td>#echo $fail['nodata'].get('count', 0) or $blank#</td>
<td>#echo $fail['other'].get('count', 0) or $blank#</td>
#if $prov['has_limit']
<td>#echo $fail.get('limit', {}).get('count', 0) or $blank#</td>
#end if
</tr>
#end for
</tbody>
</table>
<!-- /$prov['name'] -->
#end if
#end for
#end if
</div>
##
##
#end if
#if 'failure' not in $layout
</tbody>
</table>
#end if
#include $os.path.join($sg_str('PROG_DIR'), 'gui/slick/interfaces/default/inc_bottom.tmpl')

View file

@ -1,6 +1,7 @@
#import sickbeard
#import datetime
#from sickbeard import common
#from sickbeard.common import (Overview, statusStrings, ARCHIVED, FAILED, IGNORED, SKIPPED,
SNATCHED, SNATCHED_ANY, UNKNOWN, WANTED, DOWNLOADED)
##
#set global $title = 'Episode Overview'
#set global $header = 'Episode Overview'
@ -19,7 +20,7 @@
#if not $whichStatus or ($whichStatus and not $ep_counts)
##
#if $whichStatus:
<h3>no episodes have status <span class="grey-text">$common.statusStrings[$whichStatus].lower()</span></h3>
<h3>no episodes have status <span class="grey-text">$statusStrings[$whichStatus].lower()</span></h3>
#end if
<form action="$sbRoot/manage/episodeStatuses" method="get">
@ -27,8 +28,8 @@
Manage episodes with status
<select name="whichStatus" class="form-control form-control-inline input-sm" style="margin:0 10px">
#for $curStatus in [$common.SKIPPED, $common.UNKNOWN, $common.SNATCHED, $common.WANTED, $common.ARCHIVED, $common.IGNORED, $common.DOWNLOADED]:
<option value="$curStatus"#echo ('', ' selected="selected"')[$curStatus == $default_manage]#>$common.statusStrings[$curStatus]</option>
#for $curStatus in [$SKIPPED, $UNKNOWN, $SNATCHED, $WANTED, $ARCHIVED, $IGNORED, $DOWNLOADED]:
<option value="$curStatus"#echo ('', ' selected="selected"')[$curStatus == $default_manage]#>$statusStrings[$curStatus]</option>
#end for
</select>
@ -36,26 +37,26 @@
</form>
##
#else
#if $whichStatus in ($common.ARCHIVED, $common.IGNORED):
#if $whichStatus in ($ARCHIVED, $IGNORED):
#set $row_class = 'good'
#elif $whichStatus == $common.SNATCHED:
#elif $whichStatus == $SNATCHED:
#set $row_class = 'snatched'
#else
#set $row_class = $common.Overview.overviewStrings[$whichStatus]
#set $row_class = $Overview.overviewStrings[$whichStatus]
#end if
#set $statusList = [$common.SKIPPED, $common.ARCHIVED, $common.IGNORED]
#if $common.DOWNLOADED == $whichStatus:
#set $statusList = [$common.ARCHIVED]
#elif $common.ARCHIVED == $whichStatus:
#set $statusList = [$common.SKIPPED, $common.DOWNLOADED, $common.ARCHIVED, $common.IGNORED]
#set $statusList = [$ARCHIVED, $IGNORED, $SKIPPED]
#if $DOWNLOADED == $whichStatus:
#set $statusList = [$ARCHIVED]
#elif $ARCHIVED == $whichStatus:
#set $statusList = [$SKIPPED, $DOWNLOADED, $ARCHIVED, $IGNORED]
#end if
#if $whichStatus in $statusList
$statusList.remove($whichStatus)
#end if
#if $whichStatus in $common.SNATCHED_ANY
$statusList.append($common.FAILED)
#if $whichStatus in $SNATCHED_ANY
$statusList.append($FAILED)
#end if
<script type="text/javascript" src="$sbRoot/js/manageEpisodeStatuses.js?v=$sbPID"></script>
@ -63,7 +64,7 @@
<form action="$sbRoot/manage/changeEpisodeStatuses" method="post">
<input type="hidden" id="oldStatus" name="oldStatus" value="$whichStatus">
<h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$common.statusStrings[$whichStatus].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3>
<h3><span class="grey-text">$ep_count</span> episode#echo ('s', '')[1 == $ep_count]# marked <span class="grey-text">$statusStrings[$whichStatus].lower()</span> in <span class="grey-text">${len($sorted_show_ids)}</span> show#echo ('s', '')[1 == len($sorted_show_ids)]#</h3>
<input type="hidden" id="row_class" value="$row_class">
@ -71,16 +72,16 @@
<span>Set checked shows/episodes to</span>
<select name="newStatus" class="form-control form-control-inline input-sm" style="margin:0 10px 0 5px">
#for $curStatus in $statusList:
<option value="$curStatus">$common.statusStrings[$curStatus]</option>
<option value="$curStatus">$statusStrings[$curStatus]</option>
#end for
</select>
<input class="btn btn-inline go" type="submit" value="Go">
#if $common.DOWNLOADED != $whichStatus:
#if $DOWNLOADED != $whichStatus:
<span class="red-text" style="margin:0 0 0 30px">Override checked status to</span>
<select name="wantedStatus" class="form-control form-control-inline input-sm" style="margin:0 10px 0 5px">
<option value="$common.UNKNOWN">nothing</option>
<option value="$common.WANTED">$common.statusStrings[$common.WANTED]</option>
<option value="$UNKNOWN">nothing</option>
<option value="$WANTED">$statusStrings[$WANTED]</option>
</select>
<input class="btn btn-inline go" type="submit" value="Go">
#end if

View file

@ -63,83 +63,6 @@
<div id="provider-failures" class="section">
<h3>Provider Failures:</h3>
#if not $provider_fails
<p>No current failures. Failure stats display here when appropriate.</p>
#else
<p>Some providers can be often down over periods, SickGear will back off then retry connecting at a later time</p>
#for $prov in $provider_fail_stats
#if $len($prov['fails'])
<!-- $prov['name'] -->
<div>
<input type="button" class="shows-more btn" value="Expand" style="display:none"><input type="button" class="shows-less btn" value="Collapse"><img src="$sbRoot/images/providers/$prov['prov_img']" width="16" height="16" style="margin:0 6px 0 3px">$prov['name']
#if $prov['active']
#if $prov['next_try']
#set nt = $str($prov['next_try']).split('.', 2)
... is blocked until $sbdatetime.sbdatetime.sbftime($sbdatetime.sbdatetime.now() + $prov['next_try'], markup=True) (in $nt[0]) <input type="button" class="provider-retry btn" id="$prov['prov_id']-btn-retry" value="Ignore block on next search">
#end if
#else
... is not enabled
#end if
</div>
<table class="manageTable provider-failures tablesorter hover-highlight focus-highlight text-center" cellspacing="0" border="0" cellpadding="0">
<thead>
<tr>
<th class="text-center" style="width:13em;padding-right:20px">period of 1hr</th>
<th class="text-center" style="padding-right:20px">server/timeout</th>
<th class="text-center" style="padding-right:20px">network</th>
<th class="text-center" style="padding-right:20px">no data</th>
<th class="text-center" style="padding-right:20px">other</th>
#if $prov['has_limit']
<th class="text-center" style="padding-right:20px">hit limit</th>
#end if
</tr>
</thead>
<tbody>
#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#
<tr#if $fail['multirow'] and $child# class="tablesorter-childRow"#end if#>
#if $fail['multirow']
#if not $child
<td><a href="#" class="provider-fail-parent-toggle" title="Totals (expand for detail)">$sbdatetime.sbdatetime.sbfdate($fail['date_time'])</a></td>
#else
<td>$sbdatetime.sbdatetime.sbftime($fail['date_time'], markup=True)</td>
#end if
#else
<td>$sbdatetime.sbdatetime.sbfdatetime($fail['date_time'], markup=True)</td>
#end if
#set $blank = '-'
#set $title=None
#if $fail['http']['count']
#set $title=$fail['http']['code']
#end if
<td>#if $fail['http']['count']#<span title="#if $child or not $fail['multirow']#$title#else#Expand for fail codes#end if#">$fail['http']['count']</span>#else#$blank#end if# / #echo $fail['timeout'].get('count', 0) or $blank#</td>
<td>#echo ($fail['connection'].get('count', 0) + $fail['connection_timeout'].get('count', 0)) or $blank#</td>
<td>#echo $fail['nodata'].get('count', 0) or $blank#</td>
<td>#echo $fail['other'].get('count', 0) or $blank#</td>
#if $prov['has_limit']
<td>#echo $fail.get('limit', {}).get('count', 0) or $blank#</td>
#end if
</tr>
#end for
</tbody>
</table>
<!-- /$prov['name'] -->
#end if
#end for
#end if
</div>
<div id="search-queues" class="section">
<h3>Search Queues:</h3>

View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html>
<head></head>
<body>
<h1>Index of $basepath</h1>
<table border="1" cellpadding="5" cellspacing="0" class="whitelinks">
<tr>
<th>Name</th>
</tr>
#for $file in $filelist
<tr>
<td><a href="$file">$file</a></td>
</tr>
#end for
</table>
<hr>
<em>Tornado Server for SickGear</em>
</body>
</html>

View file

@ -0,0 +1,27 @@
##
#from sickbeard import WEB_PORT, WEB_ROOT, ENABLE_HTTPS
#set sg_host = $getVar('sbHost', 'localhost')
#set sg_port = str($getVar('sbHttpPort', WEB_PORT))
#set sg_root = $getVar('sbRoot', WEB_ROOT)
#set sg_use_https = $getVar('sbHttpsEnabled', ENABLE_HTTPS)
##
#set $base_url = 'http%s://%s:%s%s' % (('', 's')[any([sg_use_https])], $sg_host, $sg_port, $sg_root)
##
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="repository.sickgear" name="SickGear Add-on repository" version="1.0.0" provider-name="SickGear">
<extension point="xbmc.addon.repository"
name="SickGear Add-on Repository">
<info compressed="true">$base_url/kodi/addons.xml</info>
<checksum>$base_url/kodi/addons.xml.md5</checksum>
<datadir zip="true">$base_url/kodi</datadir>
<hashes>false</hashes>
</extension>
<extension point="xbmc.addon.metadata">
<summary>Install Add-ons for SickGear</summary>
<description>Download and install add-ons from a repository at a running SickGear instance.[CR][CR]Contains:[CR]* Watchedstate updater service</description>
<disclaimer></disclaimer>
<platform>all</platform>
<website>https://github.com/SickGear/SickGear</website>
<nofanart>true</nofanart>
</extension>
</addon>

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addons>
$watchedstate_updater_addon_xml
$repo_xml
</addons>

View file

@ -68,12 +68,12 @@ $(document).ready(function () {
var showname = document.getElementById('showtitle').getAttribute('data-showname');
$.confirm({
'title' : 'Remove Show',
'message' : 'Are you sure you want to remove <span class="footerhighlight">' + showname + '</span> from the database ?<br /><br /><input type="checkbox" id="deleteFiles"> <span class="red-text">Check to delete files as well. IRREVERSIBLE</span></input>',
'message' : 'Are you sure you want to remove <span class="footerhighlight">' + showname + '</span> from the database ?<br /><br /><input type="checkbox" id="delete-files"> <span class="red-text">Check to delete files as well. IRREVERSIBLE</span>',
'buttons' : {
'Yes' : {
'class' : 'green',
'action': function(){
location.href = target + (document.getElementById('deleteFiles').checked ? '&full=1' : '');
location.href = target + (document.getElementById('delete-files').checked ? '&full=1' : '');
// If checkbox is ticked, remove show and delete files. Else just remove show.
}
},
@ -85,6 +85,98 @@ $(document).ready(function () {
});
});
$('#del-watched').bind('click', function(e) {
e.preventDefault();
var dedupe = [], delArr = [], mFiles = 0;
$('.del-check').each(function() {
if (!0 === this.checked) {
var pathFile = $(this).closest('tr').attr('data-file'),
thisId = $(this).attr('id');
if (-1 === jQuery.inArray(pathFile, dedupe)) {
dedupe.push(pathFile);
mFiles += 1 - $(this).closest('tr').find('.tvShow .strike-deleted').length;
}
delArr.push(thisId.replace('del-', ''));
/** @namespace $.SickGear.history.isCompact */
if ($.SickGear.history.isCompact) {
// then select all related episode checkboxes
var tvepId = $(this).closest('tr').attr('data-tvep-id');
$('tr[data-tvep-id="' + tvepId + '"] input.del-check:not("#' + thisId + '")')
.each(function(){
delArr.push($(this).attr('id').replace('del-', ''));
});
}
}
});
if (0 === delArr.length)
return !1;
/** @namespace $.SickGear.history.isTrashit */
/** @namespace $.SickGear.history.lastDeleteFiles */
/** @namespace $.SickGear.history.lastDeleteRecords */
var action = $.SickGear.history.isTrashit ? 'Trash' : 'Delete',
btns = {
'Yes' : {
'class' : 'green',
'action': function(){
var deleteFiles = !!$('#delete-files:checked').length,
deleteRecords = !!$('#delete-records:checked').length,
checked = ' checked="checked"';
$.SickGear.history.lastDeleteFiles = deleteFiles ? checked : '';
$.SickGear.history.lastDeleteRecords = deleteRecords ? checked : '';
$.post($.SickGear.Root + '/history/watched',
{
tvew_id: delArr.join('|'),
files: (deleteFiles ? '1' : ''),
records: (deleteRecords ? '1' : '')
},
function(data){
var result = $.parseJSON(data);
result.success && window.location.reload(true);
/* using window.location as the following is
sluggish when deleting 20 of 100 records
*/
/*
result.success && $.each(result.success, function(){
var tr = $('#del-' + this).closest('tr');
var t = tr.closest('table');
tr.addClass('delete-me').fadeToggle('fast', 'linear').promise().done(
function(){
$('.delete-me').html('');
t.trigger('update');
$.SickGear.sumChecked();
});
});*/
});
}
}
};
// btn pre-created here in order to use a custom btn text as named key to object
btns['No' + (0 < mFiles ? ', abort ' + ($.SickGear.history.isTrashit ? 'trash' : 'permanent delete') : '')] = {'class' : 'red'};
$.confirm({
'title' : (action + (0 < mFiles ? ' media' : ' records')
+ '<span style="float:right;font-size:12px">(<a class="highlight-text contrast-text" href="/config/general/">"Send to trash" options</a>)</span>'),
'message' : (0 < mFiles
? '<input id="delete-files" style="margin-right:6px"' + $.SickGear.history.lastDeleteFiles + ' type="checkbox">'
+ '<span>' + action + ' <span class="footerhighlight">' + mFiles + '</span>'
+ ' media file' + (1===mFiles?'':'s') + ' from disk</span>'
: ''
)
+ '<span style="display:block;margin-top:20px">'
+ '<input id="delete-records" style="margin-right:6px"' + $.SickGear.history.lastDeleteRecords + ' type="checkbox">'
+ 'Remove <span class="footerhighlight">'
+ delArr.length + '</span> history record' + (1===delArr.length?'':'s')
+ '</span>'
+ '<span class="red-text" style="display:block;margin-top:20px">Are you sure ?</span>',
'buttons' : btns
});
});
$('a.clearhistory').bind('click',function(e) {
e.preventDefault();
var target = $( this ).attr('href');
@ -105,7 +197,7 @@ $(document).ready(function () {
}
});
});
$('a.trimhistory').bind('click',function(e) {
e.preventDefault();
var target = $( this ).attr('href');
@ -126,5 +218,5 @@ $(document).ready(function () {
}
});
});
});
});

236
gui/slick/js/history.js Normal file
View file

@ -0,0 +1,236 @@
/** @namespace $.SickGear.Root */
/** @namespace $.SickGear.history.isCompact */
/** @namespace $.SickGear.history.isTrashit */
/** @namespace $.SickGear.history.useSubtitles */
/** @namespace $.SickGear.history.layoutName */
/*
2017 Jason Mulligan <jason.mulligan@avoidwork.com>
@version 3.5.11
*/
!function(i){function e(i){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=[],d=0,r=void 0,a=void 0,s=void 0,f=void 0,u=void 0,l=void 0,v=void 0,B=void 0,c=void 0,p=void 0,y=void 0,m=void 0,x=void 0,g=void 0;if(isNaN(i))throw new Error("Invalid arguments");return s=!0===e.bits,y=!0===e.unix,a=e.base||2,p=void 0!==e.round?e.round:y?1:2,m=void 0!==e.spacer?e.spacer:y?"":" ",g=e.symbols||e.suffixes||{},x=2===a?e.standard||"jedec":"jedec",c=e.output||"string",u=!0===e.fullform,l=e.fullforms instanceof Array?e.fullforms:[],r=void 0!==e.exponent?e.exponent:-1,B=Number(i),v=B<0,f=a>2?1e3:1024,v&&(B=-B),(-1===r||isNaN(r))&&(r=Math.floor(Math.log(B)/Math.log(f)))<0&&(r=0),r>8&&(r=8),0===B?(n[0]=0,n[1]=y?"":t[x][s?"bits":"bytes"][r]):(d=B/(2===a?Math.pow(2,10*r):Math.pow(1e3,r)),s&&(d*=8)>=f&&r<8&&(d/=f,r++),n[0]=Number(d.toFixed(r>0?p:0)),n[1]=10===a&&1===r?s?"kb":"kB":t[x][s?"bits":"bytes"][r],y&&(n[1]="jedec"===x?n[1].charAt(0):r>0?n[1].replace(/B$/,""):n[1],o.test(n[1])&&(n[0]=Math.floor(n[0]),n[1]=""))),v&&(n[0]=-n[0]),n[1]=g[n[1]]||n[1],"array"===c?n:"exponent"===c?r:"object"===c?{value:n[0],suffix:n[1],symbol:n[1]}:(u&&(n[1]=l[r]?l[r]:b[x][r]+(s?"bit":"byte")+(1===n[0]?"":"s")),n.join(m))}var o=/^(b|B)$/,t={iec:{bits:["b","Kib","Mib","Gib","Tib","Pib","Eib","Zib","Yib"],bytes:["B","KiB","MiB","GiB","TiB","PiB","EiB","ZiB","YiB"]},jedec:{bits:["b","Kb","Mb","Gb","Tb","Pb","Eb","Zb","Yb"],bytes:["B","KB","MB","GB","TB","PB","EB","ZB","YB"]}},b={iec:["","kibi","mebi","gibi","tebi","pebi","exbi","zebi","yobi"],jedec:["","kilo","mega","giga","tera","peta","exa","zetta","yotta"]};e.partial=function(i){return function(o){return e(o,i)}},"undefined"!=typeof exports?module.exports=e:"function"==typeof define&&define.amd?define(function(){return e}):i.filesize=e}("undefined"!=typeof window?window:global);
function rowCount(){
var output$ = $('#row-count');
if(!output$.length)
return;
var tbody$ = $('#tbody'),
nRows = tbody$.find('tr').length,
compacted = tbody$.find('tr.hide').length,
compactedFiltered = tbody$.find('tr.filtered.hide').length,
filtered = tbody$.find('tr.filtered').length;
output$.text((filtered
? nRows - (filtered + compacted - compactedFiltered) + ' / ' + nRows + ' filtered'
: nRows) + (1 === nRows ? ' row' : ' rows'));
}
$(document).ready(function() {
var extraction = {0: function(node) {
var dataSort = $(node).find('div[data-sort]').attr('data-sort')
|| $(node).find('span[data-sort]').attr('data-sort');
return !dataSort ? dataSort : dataSort.toLowerCase();}},
tbody$ = $('#tbody'),
headers = {},
layoutName = '' + $.SickGear.history.layoutName;
if ('detailed' === layoutName) {
jQuery.extend(extraction, {
4: function (node) {
return $(node).find('span').text().toLowerCase();
}
});
jQuery.extend(headers, {4: {sorter: 'quality'}});
} else if ('compact' === layoutName) {
jQuery.extend(extraction, {
1: function (node) {
return $(node).find('span[data-sort]').attr('data-sort').toLowerCase();
},
2: function (node) {
return $(node).attr('provider').toLowerCase();
},
5: function (node) {
return $(node).attr('quality').toLowerCase();
}
});
var disable = {sorter: !1}, qualSort = {sorter: 'quality'};
jQuery.extend(headers, $.SickGear.history.useSubtitles ? {4: disable, 5: qualSort} : {3: disable, 4: qualSort});
} else if (-1 !== layoutName.indexOf('watched')) {
jQuery.extend(extraction, {
3: function(node) {
return $(node).find('span[data-sort]').attr('data-sort');
},
5: function(node) {
return $(node).find('span[data-sort]').attr('data-sort');
},
6: function (node) {
return $(node).find('input:checked').length;
}
});
jQuery.extend(headers, {4: {sorter: 'quality'}});
rowCount();
} else if (-1 !== layoutName.indexOf('compact_stats')) {
jQuery.extend(extraction, {
3: function (node) {
return $(node).find('div[data-sort]').attr('data-sort');
}
});
}
var isWatched = -1 !== $('select[name="HistoryLayout"]').val().indexOf('watched'),
options = {
widgets: ['zebra', 'filter'],
widgetOptions : {
filter_hideEmpty: !0, filter_matchType : {'input': 'match', 'select': 'match'},
filter_resetOnEsc: !0, filter_saveFilters: !0, filter_searchDelay: 300
},
sortList: isWatched ? [[1, 1], [0, 1]] : [0, 1],
textExtraction: extraction,
headers: headers},
stateLayoutDate = function(table$, glyph$){table$.toggleClass('event-age');glyph$.toggleClass('age date');};
if(isWatched){
jQuery.extend(options, {
selectorSort: '.tablesorter-header-inside',
headerTemplate: '<div class="tablesorter-header-inside" style="margin:0 -8px 0 -4px">{content}{icon}</div>',
onRenderTemplate: function(index, template){
if(0 === index){
template = '<i id="watched-date" class="icon-glyph date add-qtip" title="Change date layout" style="float:left;margin:4px -14px 0 2px"></i>'
+ template;
}
return template;
},
onRenderHeader: function(){
var table$ = $('#history-table'), glyph$ = $('#watched-date');
if($.tablesorter.storage(table$, 'isLayoutAge')){
stateLayoutDate(table$, glyph$);
}
$(this).find('#watched-date').on('click', function(){
stateLayoutDate(table$, glyph$);
$.tablesorter.storage(table$, 'isLayoutAge', table$.hasClass('event-age'));
return !1;
});
}
});
}
$('#history-table').tablesorter(options).bind('filterEnd', function(){
rowCount();
});
$('#limit').change(function(){
window.location.href = $.SickGear.Root + '/history/?limit=' + $(this).val()
});
$('#show-watched-help').click(function () {
$('#watched-help').fadeToggle('fast', 'linear');
$.get($.SickGear.Root + '/history/toggle_help');
});
var addQTip = (function(){
$(this).css('cursor', 'help');
$(this).qtip({
show: {solo:true},
position: {viewport:$(window), my:'left center', adjust:{y: -10, x: 2}},
style: {tip: {corner:true, method:'polygon'}, classes:'qtip-dark qtip-rounded qtip-shadow'}
});
});
$('.add-qtip').each(addQTip);
$.SickGear.sumChecked = (function(){
var dedupe = [], sum = 0, output;
$('.del-check:checked').each(function(){
if ($(this).closest('tr').find('.tvShow .strike-deleted').length)
return;
var pathFile = $(this).closest('tr').attr('data-file');
if (-1 === jQuery.inArray(pathFile, dedupe)) {
dedupe.push(pathFile);
output = $(this).closest('td').prev('td.size').find('span[data-sort]').attr('data-sort');
sum = sum + parseInt(output, 10);
}
});
$('#del-watched').attr('disabled', !dedupe.length && !$('#tbody').find('tr').find('.tvShow .strike-deleted').length);
output = filesize(sum, {symbols: {B: 'Bytes'}});
$('#sum-size').text(/\s(MB)$/.test(output) ? filesize(sum, {round:1})
: /^1\sB/.test(output) ? output.replace('Bytes', 'Byte') : output);
});
$.SickGear.sumChecked();
var className='.del-check', lastCheck = null, check, found;
tbody$.on('click', className, function(ev){
if(!lastCheck || !ev.shiftKey){
lastCheck = this;
} else {
check = this; found = 0;
$('#tbody').find('> tr:visible').find(className).each(function(){
if (2 === found)
return !1;
if (1 === found)
this.checked = lastCheck.checked;
found += (1 && (this === check || this === lastCheck));
});
}
$(this).closest('table').trigger('update');
$.SickGear.sumChecked();
});
$('.shows-less').click(function(){
var table$ = $(this).nextAll('table:first');
table$ = table$.length ? table$ : $(this).parent().nextAll('table:first');
table$.hide();
$(this).hide();
$(this).prevAll('input:first').show();
});
$('.shows-more').click(function(){
var table$ = $(this).nextAll('table:first');
table$ = table$.length ? table$ : $(this).parent().nextAll('table:first');
table$.show();
$(this).hide();
$(this).nextAll('input:first').show();
});
$('.provider-retry').click(function () {
$(this).addClass('disabled');
var match = $(this).attr('id').match(/^(.+)-btn-retry$/);
$.ajax({
url: $.SickGear.Root + '/manage/manageSearches/retryProvider?provider=' + match[1],
type: 'GET',
complete: function () {
window.location.reload(true);
}
});
});
$('.provider-failures').tablesorter({widgets : ['zebra'],
headers : { 0:{sorter:!1}, 1:{sorter:!1}, 2:{sorter:!1}, 3:{sorter:!1}, 4:{sorter:!1}, 5:{sorter:!1} }
});
$('.provider-fail-parent-toggle').click(function(){
$(this).closest('tr').nextUntil('tr:not(.tablesorter-childRow)').find('td').toggle();
return !1;
});
// Make table cell focusable
// http://css-tricks.com/simple-css-row-column-highlighting/
var focus$ = $('.focus-highlight');
if (focus$.length){
focus$.find('td, th')
.attr('tabindex', '1')
// add touch device support
.on('touchstart', function(){
$(this).focus();
});
}
});

View file

@ -34,37 +34,4 @@ $(function(){
$(this).hide();
$(this).nextAll('input:first').show();
});
$('.provider-retry').click(function () {
$(this).addClass('disabled');
var match = $(this).attr('id').match(/^(.+)-btn-retry$/);
$.ajax({
url: sbRoot + '/manage/manageSearches/retryProvider?provider=' + match[1],
type: 'GET',
complete: function () {
window.location.reload(true);
}
});
});
$('.provider-failures').tablesorter({widgets : ['zebra'],
headers : { 0:{sorter:!1}, 1:{sorter:!1}, 2:{sorter:!1}, 3:{sorter:!1}, 4:{sorter:!1}, 5:{sorter:!1} }
});
$('.provider-fail-parent-toggle').click(function(){
$(this).closest('tr').nextUntil('tr:not(.tablesorter-childRow)').find('td').toggle();
return !1;
});
// Make table cell focusable
// http://css-tricks.com/simple-css-row-column-highlighting/
var focus$ = $('.focus-highlight');
if (focus$.length){
focus$.find('td, th')
.attr('tabindex', '1')
// add touch device support
.on('touchstart', function(){
$(this).focus();
});
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
lib/plex/__init__.py Normal file
View file

@ -0,0 +1 @@
from plex import *

423
lib/plex/plex.py Normal file
View file

@ -0,0 +1,423 @@
# -*- coding: utf-8 -*-
#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
from time import sleep
import datetime
import math
import os
import platform
import re
import sys
try:
from urllib import urlencode # Python2
except ImportError:
import urllib
from urllib.parse import urlencode # Python3
try:
import urllib.request as urllib2
except ImportError:
import urllib2
from sickbeard import logger
from sickbeard.helpers import getURL, tryInt
try:
from lxml import etree
except ImportError:
try:
import xml.etree.cElementTree as etree
except ImportError:
import xml.etree.ElementTree as etree
class Plex:
def __init__(self, settings=None):
settings = settings or {}
self._plex_host = settings.get('plex_host') or '127.0.0.1'
self.plex_port = settings.get('plex_port') or '32400'
self.username = settings.get('username', '')
self.password = settings.get('password', '')
self.token = settings.get('token', '')
self.device_name = settings.get('device_name', '')
self.client_id = settings.get('client_id') or '5369636B47656172'
self.machine_client_identifier = ''
self.default_home_users = settings.get('default_home_users', '')
# Progress percentage to consider video as watched
# if set to anything > 0, videos with watch progress greater than this will be considered watched
self.default_progress_as_watched = settings.get('default_progress_as_watched', 0)
# Sections to scan. If empty all sections will be looked at,
# the section id should be used which is the number found be in the url on PlexWeb after /section/[ID]
self.section_list = settings.get('section_list', [])
# Sections to skip scanning, for use when Settings['section_list'] is not specified,
# the same as section_list, the section id should be used
self.ignore_sections = settings.get('ignore_sections', [])
# Filter sections by paths that are in this array
self.section_filter_path = settings.get('section_filter_path', [])
# Results
self.show_states = {}
self.file_count = 0
# Conf
self.config_version = 2.0
self.use_logger = False
self.test = None
self.home_user_tokens = {}
if self.username and '' == self.token:
self.token = self.get_token(self.username, self.password)
@property
def plex_host(self):
if not self._plex_host.startswith('http'):
return 'http://%s' % self.plex_host
return self._plex_host
@plex_host.setter
def plex_host(self, value):
self._plex_host = value
def log(self, msg, debug=True):
try:
if self.use_logger:
msg = 'Plex:: ' + msg
if debug:
logger.log(msg, logger.DEBUG)
else:
logger.log(msg)
# else:
# print(msg.encode('ascii', 'replace').decode())
except (StandardError, Exception):
pass
def get_token(self, user, passw):
auth = ''
try:
auth = getURL('https://plex.tv/users/sign_in.json',
headers={'X-Plex-Device-Name': 'SickGear',
'X-Plex-Platform': platform.system(), 'X-Plex-Device': platform.system(),
'X-Plex-Platform-Version': platform.release(),
'X-Plex-Provides': 'Python', 'X-Plex-Product': 'Python',
'X-Plex-Client-Identifier': self.client_id,
'X-Plex-Version': str(self.config_version),
'X-Plex-Username': user
},
json=True,
data=urlencode({b'user[login]': user, b'user[password]': passw}).encode('utf-8')
)['user']['authentication_token']
except IndexError:
self.log('Error getting Plex Token')
return auth
def get_access_token(self, token):
resources = self.get_url_x('https://plex.tv/api/resources?includeHttps=1', token=token)
if None is resources:
return ''
devices = resources.findall('Device')
for device in devices:
if 1 == len(devices) \
or self.machine_client_identifier == device.get('clientIdentifier') \
or (self.device_name
and (self.device_name.lower() in device.get('name').lower()
or self.device_name.lower() in device.get('clientIdentifier').lower())
):
access_token = device.get('accessToken')
if not access_token:
return ''
return access_token
connections = device.findall('Connection')
for connection in connections:
if self.plex_host == connection.get('address'):
access_token = device.get('accessToken')
if not access_token:
return ''
uri = connection.get('uri')
match = re.compile('(http[s]?://.*?):(\d*)').match(uri)
if match:
self.plex_host = match.group(1)
self.plex_port = match.group(2)
return access_token
return ''
def get_plex_home_user_tokens(self):
user_tokens = {}
# check Plex is contactable
home_users = self.get_url_x('https://plex.tv/api/home/users')
if None is not home_users:
for user in home_users.findall('User'):
user_id = user.get('id')
# use empty byte data to force POST
switch_page = self.get_url_x('https://plex.tv/api/home/users/%s/switch' % user_id, data=b'')
if None is not switch_page:
home_token = 'user' == switch_page.tag and switch_page.get('authenticationToken')
if home_token:
username = switch_page.get('title')
user_tokens[username] = self.get_access_token(home_token)
return user_tokens
def get_url_x(self, url, token=None, **kwargs):
if not token:
token = self.token
if not url.startswith('http'):
url = 'http://' + url
for x in range(0, 3):
if 0 < x:
sleep(0.5)
try:
headers = {'X-Plex-Device-Name': 'SickGear',
'X-Plex-Platform': platform.system(), 'X-Plex-Device': platform.system(),
'X-Plex-Platform-Version': platform.release(),
'X-Plex-Provides': 'controller', 'X-Plex-Product': 'Python',
'X-Plex-Client-Identifier': self.client_id,
'X-Plex-Version': str(self.config_version),
'X-Plex-Token': token,
'Accept': 'application/xml'
}
if self.username:
headers.update({'X-Plex-Username': self.username})
page = getURL(url, headers=headers, **kwargs)
if page:
parsed = etree.fromstring(page)
if None is not parsed and len(parsed):
return parsed
return None
except Exception as e:
self.log('Error requesting page: %s' % e)
continue
return None
# uses the Plex API to delete files instead of system functions, useful for remote installations
def delete_file(self, media_id=0):
try:
endpoint = ('/library/metadata/%s' % str(media_id))
req = urllib2.Request('%s:%s%s' % (self.plex_host, self.plex_port, endpoint),
None, {'X-Plex-Token': self.token})
req.get_method = lambda: 'DELETE'
urllib2.urlopen(req)
except (StandardError, Exception):
return False
return True
@staticmethod
def get_media_info(video_node):
progress = 0
if None is not video_node.get('viewOffset') and None is not video_node.get('duration'):
progress = tryInt(video_node.get('viewOffset')) * 100 / tryInt(video_node.get('duration'))
for media in video_node.findall('Media'):
for part in media.findall('Part'):
file_name = part.get('file')
# if '3' > sys.version: # remove HTML quoted characters, only works in python < 3
# file_name = urllib2.unquote(file_name.encode('utf-8', errors='replace'))
# else:
file_name = urllib2.unquote(file_name)
return {'path_file': file_name, 'media_id': video_node.get('ratingKey'),
'played': int(video_node.get('viewCount') or 0), 'progress': progress}
def check_users_watched(self, users, media_id):
if not self.home_user_tokens:
self.home_user_tokens = self.get_plex_home_user_tokens()
result = {}
if 'all' in users:
users = self.home_user_tokens.keys()
for user in users:
user_media_page = self.get_url_pms('/library/metadata/%s' % media_id, token=self.home_user_tokens[user])
if None is not user_media_page:
video_node = user_media_page.find('Video')
progress = 0
if None is not video_node.get('viewOffset') and None is not video_node.get('duration'):
progress = tryInt(video_node.get('viewOffset')) * 100 / tryInt(video_node.get('duration'))
played = int(video_node.get('viewCount') or 0)
if not progress and not played:
continue
date_watched = 0
if (0 < tryInt(video_node.get('viewCount'))) or (0 < self.default_progress_as_watched < progress):
last_viewed_at = video_node.get('lastViewedAt')
if last_viewed_at and last_viewed_at not in ('', '0'):
date_watched = last_viewed_at
if date_watched:
result[user] = dict(played=played, progress=progress, date_watched=date_watched)
else:
self.log('Do not have the token for %s.' % user)
return result
def get_url_pms(self, endpoint=None, **kwargs):
return endpoint and self.get_url_x(
'%s:%s%s' % (self.plex_host, self.plex_port, endpoint), **kwargs)
# parse episode information from season pages
def stat_show(self, node):
episodes = []
if 'directory' == node.tag.lower() and 'show' == node.get('type'):
show = self.get_url_pms(node.get('key'))
if None is show: # Check if show page is None or empty
self.log('Failed to load show page. Skipping...')
return None
for season_node in show.findall('Directory'): # Each directory is a season
if 'season' != season_node.get('type'): # skips Specials
continue
season_key = season_node.get('key')
season = self.get_url_pms(season_key)
if None is not season:
episodes += [season]
elif 'mediacontainer' == node.tag.lower() and 'episode' == node.get('viewGroup'):
episodes = [node]
check_users = []
if self.default_home_users:
check_users = self.default_home_users.strip(' ,').lower().split(',')
for k in range(0, len(check_users)): # Remove extra spaces and commas
check_users[k] = check_users[k].strip(', ')
for episode_node in episodes:
for video_node in episode_node.findall('Video'):
media_info = self.get_media_info(video_node)
if check_users:
user_info = self.check_users_watched(check_users, media_info['media_id'])
for user_name, user_media_info in user_info.items():
self.show_states.update({len(self.show_states): dict(
path_file=media_info['path_file'],
media_id=media_info['media_id'],
played=(100 * user_media_info['played']) or user_media_info['progress'] or 0,
label=user_name,
date_watched=user_media_info['date_watched'])})
else:
self.show_states.update({len(self.show_states): dict(
path_file=media_info['path_file'],
media_id=media_info['media_id'],
played=(100 * media_info['played']) or media_info['progress'] or 0,
label=self.username,
date_watched=video_node.get('lastViewedAt'))})
self.file_count += 1
return True
def fetch_show_states(self, fetch_all=False):
error_log = []
self.show_states = {}
server_check = self.get_url_pms('/')
if None is server_check or 'MediaContainer' != server_check.tag:
error_log.append('Cannot reach server!')
else:
if not self.device_name:
self.device_name = server_check.get('friendlyName')
if not self.machine_client_identifier:
self.machine_client_identifier = server_check.get('machineIdentifier')
access_token = None
if self.token:
access_token = self.get_access_token(self.token)
if access_token:
self.token = access_token
if not self.home_user_tokens:
self.home_user_tokens = self.get_plex_home_user_tokens()
else:
error_log.append('Access Token not found')
resp_sections = None
if None is access_token or len(access_token):
resp_sections = self.get_url_pms('/library/sections/')
if None is not resp_sections:
unpather = []
for loc in self.section_filter_path:
loc = re.sub(r'[/\\]+', '/', loc.lower())
loc = re.sub(r'^(.{,2})[/\\]', '', loc)
unpather.append(loc)
self.section_filter_path = unpather
for section in resp_sections.findall('Directory'):
if 'show' != section.get('type') or not section.findall('Location'):
continue
section_path = re.sub(r'[/\\]+', '/', section.find('Location').get('path').lower())
section_path = re.sub(r'^(.{,2})[/\\]', '', section_path)
if not any([section_path in path for path in self.section_filter_path]):
continue
if section.get('key') not in self.ignore_sections \
and section.get('title') not in self.ignore_sections:
section_key = section.get('key')
for (user, token) in (self.home_user_tokens or {'': None}).iteritems():
self.username = user
resp_section = self.get_url_pms('/library/sections/%s/%s' % (
section_key, ('recentlyViewed', 'all')[fetch_all]), token=token)
if None is not resp_section:
view_group = 'MediaContainer' == resp_section.tag and \
resp_section.get('viewGroup') or ''
if 'show' == view_group and fetch_all:
for DirectoryNode in resp_section.findall('Directory'):
self.stat_show(DirectoryNode)
elif 'episode' == view_group and not fetch_all:
self.stat_show(resp_section)
if 0 < len(error_log):
self.log('Library errors...')
for item in error_log:
self.log(item)
return 0 < len(error_log)

103
readme.md
View file

@ -1,57 +1,76 @@
<hr>
<div><a id="top"><img alt="SickGear" width="200" src="https://raw.githubusercontent.com/wiki/SickGear/SickGear.Wiki/images/SickGearLogo.png"></a></div>
**SickGear**, a usenet and bittorrent PVR
*SickGear*, a usenet and bittorrent PVR
<hr>
_Please note you should know how to use git and setup basic requirements in order to run this software._
SickGear provides management of TV shows and/or Anime, it can detect new episodes, link to downloader apps, and more. SickGear is a proud descendant of Sick Beard and is humbled to have been endorsed by one of its former lead developers.
SickGear provides management of TV shows and/or Anime, it detects new episodes, links downloader apps, and more.
Why SickGear?
* SickGear maintains perfect uptime with the longest track record of being stable, reliable and trusted to work
* SickGear delivers quality from active development with a wealth of options on a dark or light themed interface
#### Why SickGear?
* SickGear maintains a perfect uptime with the longest track record of being stable, reliable and trusted to work
* SickGear delivers valued quality from active development with a wealth of options on a dark or light themed interface
#### What now?
* [Install guides](https://github.com/SickGear/SickGear/wiki/Installation-Instructions) for many platforms
* [Migrating](https://github.com/SickGear/SickGear/wiki/Install-SickGear-%5B0%5D-Migrate) to a hassle free and feature rich set up is super simple
## Features include
Or read more below...
## Some innovative SickGear features
* Ideas of shows to add from Trakt, IMDb, and AniDB categories; anticipating, new seasons, new shows, popular, and more
* Advanced add show finder that near always returns results sorted A-Z, Aired, or Relevancy, with known shows separated
* Releases can be searched during adding a show (e.g. optionally fetch oldest and/or newest without further effort)
* Advanced automated search that always works to prevent you wasting time manually scanning result lists
* Choose to delete watched episodes from a list built directly from played media at Kodi, Emby, and/or Plex (No Trakt!)
* Smart custom qualities selector system that helps achieve an optimal quality selection for automated episode search
* Choose to have episodes upgraded in quality, or keep existing archive quality, and upgrade future episodes either way
* Natively use a most powerful regex pattern matching system for superior information handling
* Select a UI style anytime; Regular, Proview I, or Proview II - independently for Episode View, and for Display Show
* Smart fanart system allows you to rate avoid/prefer. UI can be moved or toggled off/on to fully appreciate a fanart
* Episode View Layout "Day by Day" displays a fanart background from randomly selected imminent releases
* Configure from 0 to 500 fanart images to cache per show, default is 3.
* Provider server failure charts display what failed, when and why
* Provider server failure handler that pauses connecting the more a server fails
* Provider server activity graph and stats display how each are performing
* Built-in providers that have proved over time to deliver leading durability among its application class and third parties
* Built-in Kodi repository service to maintain SickGear add-ons, the first add-on is Watched State Updater
* Separate Plex server and Plex client settings, multiple LAN Plex server support, and Plex Home multiple user support
* Intelligent library updates target the actual Plex server that carries the show of an episode (multi LAN server)
* Communicate directly with NZBGet using a dedicated integration script
* Communicate directly with qBittorrent/Deluge/Transmission etc. using a dedicated integration script
* Proxy auto-config (PAC) support to define when a proxy is used instead of using one for every system request
* Change file date, to the date that the episode aired (yup, the feature was first created here for XBMC file sorting)
* Visual percentage progress of managed episodes
* Configure an episode status for removed media files
* Configurable default home page
Features above link to the UI; innovations also exist in core, we often inspire imitators, but you can [get the real deal!](https://github.com/SickGear/SickGear/wiki/Installation-Instructions)
Other features and worthy points;
* Stable, quality assured testing and development cycle
* Innovations that inspire imitators
* Compatible with any platform via a familiar web interface
* Most comprehensive selection of usenet and torrent sources
* Compatible with any platform via a familiar web interface
* Episode management
* Group shows into personalised sections
* View missed and upcoming shows at a glance with "day by day" and other layouts
* Group shows into personalised sections in a full show list view
* Automatic and manual search for availability of wanted episodes
* Set what episodes you want and how to receive them
* Uses well known established index sites to gather show information
* Searches for known alternatively named shows with a fallback to user edited names
* Searches for known alternatively numbered episodes with a fallback to user edited numbers
* Searches for known alternatively numbered seasons with a fallback to user edited numbers
* Forward search results to a downloader (e.g. NZBGet, SABNZBd, uTorrent, and others)
* Save search results to a "blackhole" folder that can be periodically scanned for taking action
* Post-process downloaded episodes into customisable layouts, with or without extra metadata
* Advanced Failed Download Handling (FDH)
* Overview of seasons, episodes, rating, version, airdate, episode status ([their meaning](https://github.com/SickGear/SickGear/wiki/Status-Modes))
* Processing nzb/torrents with your downloader application at your chosen qualities
* Subtitle management
* Automatic and manual search for availability of episodes you want
* Well known established sources are used to gather reliable show information
* Known and user added alternative show names can be searched
* Known and user added alternative numbered seasons and/or episodes can be searched
* Forward search results directly to clients like NZBGet, SABNZBd, qBitTorrent, Deluge and others
* Save search results to a "blackhole" folder, a place designated for clients to auto scan
* Built-in post processing of episodes, with renaming to custom folder and/or file names
* Fetch metadata like fanart, poster and banner images, nfo's for clients like Kodi, Plex and many more
* Native advanced automated handling of failed downloads to ensure a success after failure
* Overview of episodes, rating, version, airdate, episode status ([their meaning](https://github.com/SickGear/SickGear/wiki/Status-Modes))
* Support for specials and multi episode media files
* Automated subtitle management
* Notification
* Home Theater/NAS (Emby, Kodi, Plex, Syno, Tivo, and more)
* Home Theater/NAS (Emby, Kodi, Plex, Syno, Tivo, and more) can be notified to update their library
* Social notifiers (Trakt, Slack, Gitter, Discord, E-mail, and more)
* Device notifiers (Boxcar2, Notify My Android, Growl, Prowl, and more)
* Server friendly with minimal number of calls (e.g. one request per chosen snatch, not per result)
* Can recommend trendy and/or personally tailored shows from Trakt, IMDb, AniDB
* Server friendly with minimal number of API calls using both active and passive search tech
* Automated alternative show names and episode numbering from XEM
Some of our innovative features;
* Automated search after adding a show
* Desktop notifications
* Enhanced Anime features when adding shows
* Visual percentage progress of managed episodes
* Separate Plex server and Plex client settings
* Intelligent library updates that target Plex servers that list the show of an episode
* Configurable episode status for removed media files
* Configurable default home page
* Source providers
* User Interface
## Screenies
<table><thead></thead><tbody>
<tr align="center">
@ -103,15 +122,15 @@ If your pull request is a new feature, please try explaining its function to a l
## Contributors
#### To core
MidgetSpy, zoggy, 1337, Tolstyak, Mr_Orange, Bricky, JackDandy, Prinz23, Supremicus, adam111316, and zanaga
MidgetSpy, zoggy, 1337, Tolstyak, Mr_Orange, Bricky, JackDandy, Prinz23, Supremicus, adam111316, and zanaga
#### In other areas, testers and QA
Warm thanks to tehspede, CtrlAltDefeat, Mike, vergessen (betrayed), and Rawh for their keen eye, all around help, and making sure all manner of things work as expected
Warm thanks to tehspede, CtrlAltDefeat, Mike, vergessen (betrayed), and Rawh for their keen eye, all around help, and making sure all manner of things work as expected
#### Provider code
Thanks also, to unsung heroes that added source providers; Idan Gutman, Daniel Heimans, jkaberg, and Seedboy
Thanks also, to unsung heroes that added source providers; Idan Gutman, Daniel Heimans, jkaberg, and Seedboy
Finally, a massive thanks to all those that remain in the shadows, the quiet ones who welcome folk to special places, we salute you for your hospitality and for tirelessly keeping up operations.
Finally, a massive thanks to all those that remain in the shadows, the quiet ones who welcome folk to special places, we salute you for your hospitality and for tirelessly keeping up operations.
## Community
* <a href="https://gitter.im/SickGear/"><img src="https://badges.gitter.im/sickgear.svg"></a> web based (most likely one on one with a dev)

View file

@ -37,12 +37,14 @@ sys.path.insert(1, os.path.abspath('../lib'))
from sickbeard import helpers, encodingKludge as ek
from sickbeard import db, image_cache, logger, naming, metadata, providers, scene_exceptions, scene_numbering, \
scheduler, auto_post_processer, search_queue, search_propers, search_recent, search_backlog, \
show_queue, show_updater, subtitles, traktChecker, version_checker, indexermapper, classes, properFinder
from sickbeard.config import CheckSection, check_setting_int, check_setting_str, ConfigMigrator, minimax
show_queue, show_updater, subtitles, traktChecker, version_checker, indexermapper, classes, properFinder, \
watchedstate_queue
from sickbeard.config import check_section, check_setting_int, check_setting_str, ConfigMigrator, minimax
from sickbeard.common import SD, SKIPPED
from sickbeard.databases import mainDB, cache_db, failed_db
from sickbeard.exceptions import ex
from sickbeard.providers.generic import GenericProvider
from sickbeard.watchedstate import EmbyWatchedStateUpdater, PlexWatchedStateUpdater
from indexers.indexer_config import INDEXER_TVDB
from indexers.indexer_api import indexerApi
from indexers.indexer_exceptions import indexer_shownotfound, indexer_exception, indexer_error, \
@ -85,6 +87,9 @@ autoPostProcesserScheduler = None
subtitlesFinderScheduler = None
# traktCheckerScheduler = None
background_mapping_task = None
embyWatchedStateScheduler = None
plexWatchedStateScheduler = None
watchedStateQueueScheduler = None
provider_ping_thread_pool = {}
@ -226,12 +231,15 @@ DEFAULT_AUTOPOSTPROCESSER_FREQUENCY = 10
DEFAULT_RECENTSEARCH_FREQUENCY = 40
DEFAULT_BACKLOG_FREQUENCY = 21
DEFAULT_UPDATE_FREQUENCY = 1
DEFAULT_WATCHEDSTATE_FREQUENCY = 10
MIN_AUTOPOSTPROCESSER_FREQUENCY = 1
MIN_RECENTSEARCH_FREQUENCY = 10
MIN_BACKLOG_FREQUENCY = 7
MAX_BACKLOG_FREQUENCY = 42
MIN_UPDATE_FREQUENCY = 1
MIN_WATCHEDSTATE_FREQUENCY = 10
MAX_WATCHEDSTATE_FREQUENCY = 60
BACKLOG_DAYS = 7
SEARCH_UNAIRED = False
@ -279,6 +287,8 @@ USE_EMBY = False
EMBY_UPDATE_LIBRARY = False
EMBY_HOST = None
EMBY_APIKEY = None
EMBY_WATCHEDSTATE_SCHEDULED = False
EMBY_WATCHEDSTATE_FREQUENCY = None
USE_KODI = False
KODI_ALWAYS_ON = True
@ -301,6 +311,8 @@ PLEX_SERVER_HOST = None
PLEX_HOST = None
PLEX_USERNAME = None
PLEX_PASSWORD = None
PLEX_WATCHEDSTATE_SCHEDULED = False
PLEX_WATCHEDSTATE_FREQUENCY = None
USE_XBMC = False
XBMC_ALWAYS_ON = True
@ -566,8 +578,10 @@ def initialize(console_logging=True):
# global traktCheckerScheduler
global recentSearchScheduler, backlogSearchScheduler, showUpdateScheduler, \
versionCheckScheduler, showQueueScheduler, searchQueueScheduler, \
properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, background_mapping_task, \
provider_ping_thread_pool
properFinderScheduler, autoPostProcesserScheduler, subtitlesFinderScheduler, \
background_mapping_task, provider_ping_thread_pool, \
embyWatchedStateScheduler, plexWatchedStateScheduler, watchedStateQueueScheduler, \
MIN_WATCHEDSTATE_FREQUENCY, MAX_WATCHEDSTATE_FREQUENCY, DEFAULT_WATCHEDSTATE_FREQUENCY
# Add Show Search
global RESULTS_SORTBY
# Add Show Defaults
@ -626,6 +640,7 @@ def initialize(console_logging=True):
METADATA_PS3, METADATA_TIVO, METADATA_WDTV, METADATA_XBMC_12PLUS
# Notification Settings/HT and NAS
global USE_EMBY, EMBY_UPDATE_LIBRARY, EMBY_HOST, EMBY_APIKEY, \
EMBY_WATCHEDSTATE_SCHEDULED, EMBY_WATCHEDSTATE_FREQUENCY, \
USE_KODI, KODI_ALWAYS_ON, KODI_UPDATE_LIBRARY, KODI_UPDATE_FULL, KODI_UPDATE_ONLYFIRST, \
KODI_HOST, KODI_USERNAME, KODI_PASSWORD, KODI_NOTIFY_ONSNATCH, \
KODI_NOTIFY_ONDOWNLOAD, KODI_NOTIFY_ONSUBTITLEDOWNLOAD, \
@ -633,6 +648,7 @@ def initialize(console_logging=True):
XBMC_UPDATE_LIBRARY, XBMC_UPDATE_FULL, XBMC_UPDATE_ONLYFIRST, XBMC_HOST, XBMC_USERNAME, XBMC_PASSWORD, \
USE_PLEX, PLEX_USERNAME, PLEX_PASSWORD, PLEX_UPDATE_LIBRARY, PLEX_SERVER_HOST, \
PLEX_NOTIFY_ONSNATCH, PLEX_NOTIFY_ONDOWNLOAD, PLEX_NOTIFY_ONSUBTITLEDOWNLOAD, PLEX_HOST, \
PLEX_WATCHEDSTATE_SCHEDULED, PLEX_WATCHEDSTATE_FREQUENCY, \
USE_NMJ, NMJ_HOST, NMJ_DATABASE, NMJ_MOUNT, \
USE_NMJv2, NMJv2_HOST, NMJv2_DATABASE, NMJv2_DBLOC, \
USE_SYNOINDEX, \
@ -680,7 +696,7 @@ def initialize(console_logging=True):
'Growl', 'Prowl', 'Twitter', 'Slack', 'Discordapp', 'Boxcar2', 'NMJ', 'NMJv2',
'Synology', 'SynologyNotifier',
'pyTivo', 'NMA', 'Pushalot', 'Pushbullet', 'Subtitles'):
CheckSection(CFG, stanza)
check_section(CFG, stanza)
update_config = False
@ -933,6 +949,10 @@ def initialize(console_logging=True):
EMBY_UPDATE_LIBRARY = bool(check_setting_int(CFG, 'Emby', 'emby_update_library', 0))
EMBY_HOST = check_setting_str(CFG, 'Emby', 'emby_host', '')
EMBY_APIKEY = check_setting_str(CFG, 'Emby', 'emby_apikey', '')
EMBY_WATCHEDSTATE_SCHEDULED = bool(check_setting_int(CFG, 'Emby', 'emby_watchedstate_scheduled', 0))
EMBY_WATCHEDSTATE_FREQUENCY = minimax(check_setting_int(
CFG, 'Emby', 'emby_watchedstate_frequency', DEFAULT_WATCHEDSTATE_FREQUENCY),
DEFAULT_WATCHEDSTATE_FREQUENCY, MIN_WATCHEDSTATE_FREQUENCY, MAX_WATCHEDSTATE_FREQUENCY)
USE_KODI = bool(check_setting_int(CFG, 'Kodi', 'use_kodi', 0))
KODI_ALWAYS_ON = bool(check_setting_int(CFG, 'Kodi', 'kodi_always_on', 1))
@ -967,6 +987,10 @@ def initialize(console_logging=True):
PLEX_HOST = check_setting_str(CFG, 'Plex', 'plex_host', '')
PLEX_USERNAME = check_setting_str(CFG, 'Plex', 'plex_username', '')
PLEX_PASSWORD = check_setting_str(CFG, 'Plex', 'plex_password', '')
PLEX_WATCHEDSTATE_SCHEDULED = bool(check_setting_int(CFG, 'Plex', 'plex_watchedstate_scheduled', 0))
PLEX_WATCHEDSTATE_FREQUENCY = minimax(check_setting_int(
CFG, 'Plex', 'plex_watchedstate_frequency', DEFAULT_WATCHEDSTATE_FREQUENCY),
DEFAULT_WATCHEDSTATE_FREQUENCY, MIN_WATCHEDSTATE_FREQUENCY, MAX_WATCHEDSTATE_FREQUENCY)
USE_GROWL = bool(check_setting_int(CFG, 'Growl', 'use_growl', 0))
GROWL_NOTIFY_ONSNATCH = bool(check_setting_int(CFG, 'Growl', 'growl_notify_onsnatch', 0))
@ -1049,7 +1073,6 @@ def initialize(console_logging=True):
TRAKT_ACCOUNTS = TraktAPI.read_config_string(check_setting_str(CFG, 'Trakt', 'trakt_accounts', ''))
TRAKT_MRU = check_setting_str(CFG, 'Trakt', 'trakt_mru', '')
CheckSection(CFG, 'pyTivo')
USE_PYTIVO = bool(check_setting_int(CFG, 'pyTivo', 'use_pytivo', 0))
PYTIVO_HOST = check_setting_str(CFG, 'pyTivo', 'pytivo_host', '')
PYTIVO_SHARE_NAME = check_setting_str(CFG, 'pyTivo', 'pytivo_share_name', '')
@ -1418,6 +1441,23 @@ def initialize(console_logging=True):
background_mapping_task = threading.Thread(name='LOAD-MAPPINGS', target=indexermapper.load_mapped_ids)
watchedStateQueueScheduler = scheduler.Scheduler(
watchedstate_queue.WatchedStateQueue(),
cycleTime=datetime.timedelta(seconds=3),
threadName='WATCHEDSTATEQUEUE')
embyWatchedStateScheduler = scheduler.Scheduler(
EmbyWatchedStateUpdater(),
cycleTime=datetime.timedelta(minutes=EMBY_WATCHEDSTATE_FREQUENCY),
run_delay=datetime.timedelta(minutes=5),
threadName='EMBYWATCHEDSTATE')
plexWatchedStateScheduler = scheduler.Scheduler(
PlexWatchedStateUpdater(),
cycleTime=datetime.timedelta(minutes=PLEX_WATCHEDSTATE_FREQUENCY),
run_delay=datetime.timedelta(minutes=5),
threadName='PLEXWATCHEDSTATE')
__INITIALIZED__ = True
return True
@ -1427,7 +1467,8 @@ def enabled_schedulers(is_init=False):
for s in ([], [events])[is_init] + \
[recentSearchScheduler, backlogSearchScheduler, showUpdateScheduler,
versionCheckScheduler, showQueueScheduler, searchQueueScheduler, properFinderScheduler,
autoPostProcesserScheduler, subtitlesFinderScheduler] + \
autoPostProcesserScheduler, subtitlesFinderScheduler,
embyWatchedStateScheduler, plexWatchedStateScheduler, watchedStateQueueScheduler] + \
([events], [])[is_init]:
yield s
@ -1737,6 +1778,8 @@ def save_config():
new_config['Emby']['emby_update_library'] = int(EMBY_UPDATE_LIBRARY)
new_config['Emby']['emby_host'] = EMBY_HOST
new_config['Emby']['emby_apikey'] = EMBY_APIKEY
new_config['Emby']['emby_watchedstate_scheduled'] = int(EMBY_WATCHEDSTATE_SCHEDULED)
new_config['Emby']['emby_watchedstate_frequency'] = int(EMBY_WATCHEDSTATE_FREQUENCY)
new_config['Kodi'] = {}
new_config['Kodi']['use_kodi'] = int(USE_KODI)
@ -1761,6 +1804,8 @@ def save_config():
new_config['Plex']['plex_notify_ondownload'] = int(PLEX_NOTIFY_ONDOWNLOAD)
new_config['Plex']['plex_notify_onsubtitledownload'] = int(PLEX_NOTIFY_ONSUBTITLEDOWNLOAD)
new_config['Plex']['plex_host'] = PLEX_HOST
new_config['Plex']['plex_watchedstate_scheduled'] = int(PLEX_WATCHEDSTATE_SCHEDULED)
new_config['Plex']['plex_watchedstate_frequency'] = int(PLEX_WATCHEDSTATE_FREQUENCY)
new_config['XBMC'] = {}
new_config['XBMC']['use_xbmc'] = int(USE_XBMC)

View file

@ -23,21 +23,22 @@ from sickbeard import logger, processTV
from sickbeard import encodingKludge as ek
class PostProcesser():
class PostProcesser:
def __init__(self):
self.amActive = False
@staticmethod
def check_paused():
if sickbeard.PROCESS_AUTOMATICALLY:
return False
return True
def is_enabled():
return sickbeard.PROCESS_AUTOMATICALLY
def run(self):
if not sickbeard.PROCESS_AUTOMATICALLY:
return
if self.is_enabled():
self.amActive = True
self._main()
self.amActive = False
self.amActive = True
@staticmethod
def _main():
if not ek.ek(os.path.isdir, sickbeard.TV_DOWNLOAD_DIR):
logger.log(u"Automatic post-processing attempted but dir %s doesn't exist" % sickbeard.TV_DOWNLOAD_DIR,
@ -50,5 +51,3 @@ class PostProcesser():
return
processTV.processDir(sickbeard.TV_DOWNLOAD_DIR, is_basedir=True)
self.amActive = False

View file

@ -17,6 +17,7 @@
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import re
import datetime
import os
import sickbeard
from sickbeard.common import Quality
@ -379,3 +380,48 @@ class ImageUrlList(list):
if self._is_cache_item(x) and url == x[0]:
super(ImageUrlList, self).remove(x)
break
if 'nt' == os.name:
import ctypes
class WinEnv:
def __init__(self):
pass
@staticmethod
def get_environment_variable(name):
name = unicode(name) # ensures string argument is unicode
n = ctypes.windll.kernel32.GetEnvironmentVariableW(name, None, 0)
result = None
if n:
buf = ctypes.create_unicode_buffer(u'\0'*n)
ctypes.windll.kernel32.GetEnvironmentVariableW(name, buf, n)
result = buf.value
return result
def __getitem__(self, key):
return self.get_environment_variable(key)
def get(self, key, default=None):
r = self.get_environment_variable(key)
return r if r is not None else default
sickbeard.ENV = WinEnv()
else:
class LinuxEnv(object):
def __init__(self, environ):
self.environ = environ
def __getitem__(self, key):
v = self.environ.get(key)
try:
return v.decode(SYS_ENCODING) if isinstance(v, str) else v
except (UnicodeDecodeError, UnicodeEncodeError):
return v
def get(self, key, default=None):
v = self[key]
return v if v is not None else default
sickbeard.ENV = LinuxEnv(os.environ)

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

View file

@ -0,0 +1,22 @@
# /tests/_devenv.py
#
# To trigger dev env
#
# import _devenv as devenv
#
__remotedebug__ = True
if __remotedebug__:
import sys
sys.path.append('C:\Program Files\JetBrains\PyCharm 2017.2.1\debug-eggs\pycharm-debug.egg')
import pydevd
def setup_devenv(state):
pydevd.settrace('localhost', port=(65001, 65000)[bool(state)], stdoutToServer=True, stderrToServer=True,
suspend=False)
def stop():
pydevd.stoptrace()

View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<addon id="service.sickgear.watchedstate.updater" name="SickGear Watched State Updater" version="1.0.3" provider-name="SickGear">
<requires>
<import addon="xbmc.python" version="2.19.0" />
<import addon="xbmc.json" version="6.20.0" />
<import addon="xbmc.addon" version="14.0.0" />
</requires>
<extension point="xbmc.service" library="service.py" start="login" />
<extension point="xbmc.python.pluginsource" library="service.py" >
<provides>executable</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en">SickGear Watched State Updater</summary>
<description lang="en">This Add-on notifies SickGear when an episode watched state is changed in Kodi</description>
<platform>all</platform>
<language>en</language>
<disclaimer/>
<license/>
<forum/>
<website>https://github.com/sickgear/sickgear</website>
<email/>
<nofanart>true</nofanart>
<source>https://github.com/sickgear/sickgear</source>
<assets>
<icon>icon.png</icon>
</assets>
<news>[B]1.0.0[/B] (2017-10-04)
- Initial release
[B]1.0.2[/B] (2017-11-15)
- Devel release for an SG API change
[B]1.0.3[/B] (2018-02-28)
- Add episodeid to payload
</news>
</extension>
</addon>

View file

@ -0,0 +1,2 @@
[B]1.0.0[/B] (2017-10-04)
- Initial release

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View file

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<strings>
<string id="32000">General</string>
<string id="32011">Action Notifications</string>
<string id="32012">Error Notifications</string>
<string id="32021">Verbose Logs</string>
<string id="32100">Servers</string>
<string id="32111">SickGear IP</string>
<string id="32112">SickGear Port</string>
<string id="32121">Kodi IP</string>
<string id="32122">Kodi JSON RPC Port</string>
<string id="32500">The following required Kodi settings should already be enabled:</string>
<string id="32511">At "System / Service(s) settings / Control (aka Remote control)"</string>
<string id="32512">* Allow remote control from/by applications/programs on this system</string>
<string id="32513">* Allow remote control from/by applications/programs on other systems</string>
</strings>

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<settings>
<category label="32000">
<setting label="32011" type="bool" id="action_notification" default="true" />
<setting label="32012" type="bool" id="error_notification" default="true" />
<setting label="32021" type="bool" id="verbose_log" default="true" />
<setting label="32500" type="lsep" />
<setting label="32511" type="lsep" />
<setting label="32512" type="lsep" />
<setting label="32513" type="lsep" />
</category>
<category label="32100">
<setting label="32111" type="ipaddress" id="sickgear_ip" default="127.0.0.1" />
<setting label="32112" type="number" id="sickgear_port" default="8081" />
<setting label="32121" type="ipaddress" id="kodi_ip" default="127.0.0.1" />
<setting label="32122" type="number" id="kodi_port" default="9090" />
</category>
</settings>

View file

@ -0,0 +1,361 @@
# coding=utf-8
#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
try:
import json as json
except (StandardError, Exception):
import simplejson as json
from os import path, sep
import datetime
import socket
import time
import traceback
import urllib
import urllib2
import xbmc
import xbmcaddon
import xbmcgui
import xbmcvfs
class SickGearWatchedStateUpdater:
def __init__(self):
self.wait_onstartup = 4000
icon_size = '%s'
try:
if 1350 > xbmcgui.Window.getWidth(xbmcgui.Window()):
icon_size += '-sm'
except (StandardError, Exception):
pass
icon = 'special://home/addons/service.sickgear.watchedstate.updater/resources/icon-%s.png' % icon_size
self.addon = xbmcaddon.Addon()
self.red_logo = icon % 'red'
self.green_logo = icon % 'green'
self.black_logo = icon % 'black'
self.addon_name = self.addon.getAddonInfo('name')
self.kodi_ip = self.addon.getSetting('kodi_ip')
self.kodi_port = int(self.addon.getSetting('kodi_port'))
self.kodi_events = None
self.sock_kodi = None
def run(self):
"""
Main start
:return:
:rtype:
"""
if not self.enable_kodi_allow_remote():
return
self.sock_kodi = socket.socket()
self.sock_kodi.setblocking(True)
xbmc.sleep(self.wait_onstartup)
try:
self.sock_kodi.connect((self.kodi_ip, self.kodi_port))
except (StandardError, Exception) as e:
return self.report_contact_fail(e)
self.log('Started')
self.notify('Started in background')
self.kodi_events = xbmc.Monitor()
sock_buffer, depth, methods, method = '', 0, {'VideoLibrary.OnUpdate': self.video_library_on_update}, None
# socks listener parsing Kodi json output into action to perform
while not self.kodi_events.abortRequested():
chunk = self.sock_kodi.recv(1)
sock_buffer += chunk
if chunk in '{}':
if '{' == chunk:
depth += 1
else:
depth -= 1
if not depth:
json_msg = json.loads(sock_buffer)
try:
method = json_msg.get('method')
method_handler = methods[method]
method_handler(json_msg)
except KeyError:
if 'System.OnQuit' == method:
break
if __dev__:
self.log('pass on event: %s' % json_msg.get('method'))
sock_buffer = ''
self.sock_kodi.close()
del self.kodi_events
self.log('Stopped')
def is_enabled(self, name):
"""
Return state of an Add-on setting as Boolean
:param name: Name of Addon setting
:type name: String
:return: Success as True if addon setting is enabled, else False
:rtype: Bool
"""
return 'true' == self.addon.getSetting(name)
def log(self, msg, error=False):
"""
Add a message to the Kodi logging system (provided setting allows it)
:param msg: Text to add to log file
:type msg: String
:param error: Specify whether text indicates an error or action
:type error: Boolean
:return:
:rtype:
"""
if self.is_enabled('verbose_log'):
xbmc.log('[%s]:: %s' % (self.addon_name, msg), (xbmc.LOGNOTICE, xbmc.LOGERROR)[error])
def notify(self, msg, period=4, error=None):
"""
Invoke the Kodi onscreen notification panel with a message (provided setting allows it)
:param msg: Text to display in panel
:type msg: String
:param period: Wait seconds before closing dialog
:type period: Integer
:param error: Specify whether text indicates an error or action
:type error: Boolean
:return:
:rtype:
"""
if not error and self.is_enabled('action_notification') or (error and self.is_enabled('error_notification')):
xbmc.executebuiltin('Notification(%s, "%s", %s, %s)' % (
self.addon_name, msg, 1000 * period,
((self.green_logo, self.red_logo)[any([error])], self.black_logo)[None is error]))
@staticmethod
def ex(e):
return '\n'.join(['\nEXCEPTION Raised: --> Python callback/script returned the following error <--',
'Error type: <type \'{0}\'>',
'Error content: {1!r}',
'{2}',
'--> End of Python script error report <--\n'
]).format(type(e).__name__, e.args, traceback.format_exc())
def report_contact_fail(self, e):
msg = 'Failed to contact Kodi at %s:%s' % (self.kodi_ip, self.kodi_port)
self.log('%s %s' % (msg, self.ex(e)), error=True)
self.notify(msg, period=20, error=True)
def kodi_request(self, params):
params.update(dict(jsonrpc='2.0', id='SickGear'))
try:
response = xbmc.executeJSONRPC(json.dumps(params))
except (StandardError, Exception) as e:
return self.report_contact_fail(e)
try:
return json.loads(response)
except UnicodeDecodeError:
return json.loads(response.decode('utf-8', 'ignore'))
def video_library_on_update(self, json_msg):
"""
Actions to perform for: Kodi Notifications / VideoLibrary/ VideoLibrary.OnUpdate
invoked in Kodi when: A video item has been updated
source: http://kodi.wiki/view/JSON-RPC_API/v8#VideoLibrary.OnUpdate
:param json_msg: A JSON parsed from socks
:type json_msg: String
:return:
:rtype:
"""
try:
# note: this is called multiple times when a season is marked as un-/watched
if 'episode' == json_msg['params']['data']['item']['type']:
media_id = json_msg['params']['data']['item']['id']
play_count = json_msg['params']['data']['playcount']
json_resp = self.kodi_request(dict(
method='Profiles.GetCurrentProfile'))
current_profile = json_resp['result']['label']
json_resp = self.kodi_request(dict(
method='VideoLibrary.GetEpisodeDetails',
params=dict(episodeid=media_id, properties=['file'])))
path_file = json_resp['result']['episodedetails']['file'].encode('utf-8')
self.update_sickgear(media_id, path_file, play_count, current_profile)
except (StandardError, Exception):
pass
def update_sickgear(self, media_id, path_file, play_count, profile):
self.notify('Update sent to SickGear')
url = 'http://%s:%s/update_watched_state_kodi/' % (
self.addon.getSetting('sickgear_ip'), self.addon.getSetting('sickgear_port'))
self.log('Notify state to %s with path_file=%s' % (url, path_file))
msg_bad = 'Failed to contact SickGear on port %s at %s' % (
self.addon.getSetting('sickgear_port'), self.addon.getSetting('sickgear_ip'))
payload_json = self.payload_prep(dict(media_id=media_id, path_file=path_file, played=play_count, label=profile))
if payload_json:
payload = urllib.urlencode(dict(payload=payload_json))
try:
rq = urllib2.Request(url, data=payload)
r = urllib2.urlopen(rq)
response = json.load(r)
r.close()
if 'OK' == r.msg:
self.payload_prep(response)
if not all(response.values()):
msg = 'Success, watched state updated'
else:
msg = 'Success, %s/%s watched stated updated' % (
len([v for v in response.values() if v]), len(response.values()))
self.log(msg)
self.notify(msg, error=False)
else:
msg_bad = 'Failed to update watched state'
self.log(msg_bad)
self.notify(msg_bad, error=True)
except (urllib2.URLError, IOError) as e:
self.log(u'Couldn\'t contact SickGear %s' % self.ex(e), error=True)
self.notify(msg_bad, error=True, period=15)
except (StandardError, Exception) as e:
self.log(u'Couldn\'t contact SickGear %s' % self.ex(e), error=True)
self.notify(msg_bad, error=True, period=15)
@staticmethod
def payload_prep(payload):
name = 'sickgear_buffer.txt'
# try to locate /temp at parent location
path_temp = path.join(path.dirname(path.dirname(path.realpath(__file__))), 'temp')
path_data = path.join(path_temp, name)
data_pool = {}
if xbmcvfs.exists(path_data):
fh = None
try:
fh = xbmcvfs.File(path_data)
data_pool = json.load(fh)
except (StandardError, Exception):
pass
fh and fh.close()
temp_ok = True
if not any([data_pool]):
temp_ok = xbmcvfs.exists(path_temp) or xbmcvfs.exists(path.join(path_temp, sep))
if not temp_ok:
temp_ok = xbmcvfs.mkdirs(path_temp)
response_data = False
for k, v in payload.items():
if response_data or k in data_pool:
response_data = True
if not v:
# whether no fail response or bad input, remove this from data
data_pool.pop(k)
elif isinstance(v, basestring):
# error so retry next time
continue
if not response_data:
ts_now = time.mktime(datetime.datetime.now().timetuple())
timeout = 100
while ts_now in data_pool and timeout:
ts_now = time.mktime(datetime.datetime.now().timetuple())
timeout -= 1
max_payload = 50-1
for k in list(data_pool.keys())[max_payload:]:
data_pool.pop(k)
payload.update(dict(date_watched=ts_now))
data_pool.update({ts_now: payload})
output = json.dumps(data_pool)
if temp_ok:
fh = None
try:
fh = xbmcvfs.File(path_data, 'w')
fh.write(output)
except (StandardError, Exception):
pass
fh and fh.close()
return output
def enable_kodi_allow_remote(self):
try:
# setting esenabled: allow remote control by programs on this system
# setting esallinterfaces: allow remote control by programs on other systems
settings = [dict(esenabled=True), dict(esallinterfaces=True)]
for setting in settings:
if not self.kodi_request(dict(
method='Settings.SetSettingValue',
params=dict(setting='services.%s' % setting.keys()[0], value=setting.values()[0])
)).get('result', {}):
settings[setting] = self.kodi_request(dict(
method='Settings.GetSettingValue',
params=dict(setting='services.%s' % setting.keys()[0])
)).get('result', {}).get('value')
except (StandardError, Exception):
return
setting_states = [setting.values()[0] for setting in settings]
if not all(setting_states):
if not (any(setting_states)):
msg = 'Please enable *all* Kodi settings to allow remote control by programs...'
else:
msg = 'Please enable Kodi setting to allow remote control by programs on other systems'
msg = 'Failed startup. %s in system service/remote control' % msg
self.log(msg, error=True)
self.notify(msg, period=20, error=True)
return
return True
__dev__ = True
if __dev__:
try:
# noinspection PyProtectedMember
import _devenv as devenv
except ImportError:
__dev__ = False
if 1 < len(sys.argv):
if __dev__:
devenv.setup_devenv(False)
if sys.argv[2].endswith('send_all'):
print('>>>>>> TESTTESTTEST')
elif __name__ == '__main__':
if __dev__:
devenv.setup_devenv(True)
WSU = SickGearWatchedStateUpdater()
WSU.run()
del WSU
if __dev__:
devenv.stop()

View file

@ -41,19 +41,6 @@ cpu_presets = {'DISABLED': 0, 'LOW': 0.01, 'NORMAL': 0.05, 'HIGH': 0.1}
MULTI_EP_RESULT = -1
SEASON_RESULT = -2
# Notification Types
NOTIFY_SNATCH = 1
NOTIFY_DOWNLOAD = 2
NOTIFY_SUBTITLE_DOWNLOAD = 3
NOTIFY_GIT_UPDATE = 4
NOTIFY_GIT_UPDATE_TEXT = 5
notifyStrings = {NOTIFY_SNATCH: 'Started Download',
NOTIFY_DOWNLOAD: 'Download Finished',
NOTIFY_SUBTITLE_DOWNLOAD: 'Subtitle Download Finished',
NOTIFY_GIT_UPDATE: 'SickGear Updated',
NOTIFY_GIT_UPDATE_TEXT: 'SickGear Updated To Commit#: '}
# Episode statuses
UNKNOWN = -1 # should never happen
UNAIRED = 1 # episodes that haven't aired yet

View file

@ -49,7 +49,7 @@ naming_sep_type = (' - ', ' ')
naming_sep_type_text = (' - ', 'space')
def change_HTTPS_CERT(https_cert):
def change_https_cert(https_cert):
if https_cert == '':
sickbeard.HTTPS_CERT = ''
return True
@ -64,7 +64,7 @@ def change_HTTPS_CERT(https_cert):
return True
def change_HTTPS_KEY(https_key):
def change_https_key(https_key):
if https_key == '':
sickbeard.HTTPS_KEY = ''
return True
@ -79,7 +79,7 @@ def change_HTTPS_KEY(https_key):
return True
def change_LOG_DIR(log_dir, web_log):
def change_log_dir(log_dir, web_log):
log_dir_changed = False
abs_log_dir = os.path.normpath(os.path.join(sickbeard.DATA_DIR, log_dir))
web_log_value = checkbox_to_value(web_log)
@ -102,7 +102,7 @@ def change_LOG_DIR(log_dir, web_log):
return True
def change_NZB_DIR(nzb_dir):
def change_nzb_dir(nzb_dir):
if nzb_dir == '':
sickbeard.NZB_DIR = ''
return True
@ -117,7 +117,7 @@ def change_NZB_DIR(nzb_dir):
return True
def change_TORRENT_DIR(torrent_dir):
def change_torrent_dir(torrent_dir):
if torrent_dir == '':
sickbeard.TORRENT_DIR = ''
return True
@ -132,7 +132,7 @@ def change_TORRENT_DIR(torrent_dir):
return True
def change_TV_DOWNLOAD_DIR(tv_download_dir):
def change_tv_download_dir(tv_download_dir):
if tv_download_dir == '':
sickbeard.TV_DOWNLOAD_DIR = ''
return True
@ -147,16 +147,17 @@ def change_TV_DOWNLOAD_DIR(tv_download_dir):
return True
def change_AUTOPOSTPROCESSER_FREQUENCY(freq):
def schedule_autopostprocesser(freq):
sickbeard.AUTOPOSTPROCESSER_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_AUTOPOSTPROCESSER_FREQUENCY)
if sickbeard.AUTOPOSTPROCESSER_FREQUENCY < sickbeard.MIN_AUTOPOSTPROCESSER_FREQUENCY:
sickbeard.AUTOPOSTPROCESSER_FREQUENCY = sickbeard.MIN_AUTOPOSTPROCESSER_FREQUENCY
sickbeard.autoPostProcesserScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.AUTOPOSTPROCESSER_FREQUENCY)
sickbeard.autoPostProcesserScheduler.set_paused_state()
def change_RECENTSEARCH_FREQUENCY(freq):
def schedule_recentsearch(freq):
sickbeard.RECENTSEARCH_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_RECENTSEARCH_FREQUENCY)
if sickbeard.RECENTSEARCH_FREQUENCY < sickbeard.MIN_RECENTSEARCH_FREQUENCY:
@ -165,13 +166,14 @@ def change_RECENTSEARCH_FREQUENCY(freq):
sickbeard.recentSearchScheduler.cycleTime = datetime.timedelta(minutes=sickbeard.RECENTSEARCH_FREQUENCY)
def change_BACKLOG_FREQUENCY(freq):
sickbeard.BACKLOG_FREQUENCY = minimax(freq, sickbeard.DEFAULT_BACKLOG_FREQUENCY, sickbeard.MIN_BACKLOG_FREQUENCY, sickbeard.MAX_BACKLOG_FREQUENCY)
def schedule_backlog(freq):
sickbeard.BACKLOG_FREQUENCY = minimax(freq, sickbeard.DEFAULT_BACKLOG_FREQUENCY,
sickbeard.MIN_BACKLOG_FREQUENCY, sickbeard.MAX_BACKLOG_FREQUENCY)
sickbeard.backlogSearchScheduler.action.cycleTime = sickbeard.BACKLOG_FREQUENCY
def change_UPDATE_FREQUENCY(freq):
def schedule_update(freq):
sickbeard.UPDATE_FREQUENCY = to_int(freq, default=sickbeard.DEFAULT_UPDATE_FREQUENCY)
if sickbeard.UPDATE_FREQUENCY < sickbeard.MIN_UPDATE_FREQUENCY:
@ -180,27 +182,25 @@ def change_UPDATE_FREQUENCY(freq):
sickbeard.versionCheckScheduler.cycleTime = datetime.timedelta(hours=sickbeard.UPDATE_FREQUENCY)
def change_VERSION_NOTIFY(version_notify):
oldSetting = sickbeard.VERSION_NOTIFY
def schedule_version_notify(version_notify):
old_setting = sickbeard.VERSION_NOTIFY
sickbeard.VERSION_NOTIFY = version_notify
if not version_notify:
sickbeard.NEWEST_VERSION_STRING = None
if not oldSetting and version_notify:
if not old_setting and version_notify:
sickbeard.versionCheckScheduler.action.run()
def change_DOWNLOAD_PROPERS(download_propers):
if sickbeard.DOWNLOAD_PROPERS == download_propers:
return
sickbeard.DOWNLOAD_PROPERS = download_propers
sickbeard.properFinderScheduler.check_paused()
def schedule_download_propers(download_propers):
if sickbeard.DOWNLOAD_PROPERS != download_propers:
sickbeard.DOWNLOAD_PROPERS = download_propers
sickbeard.properFinderScheduler.set_paused_state()
def change_USE_TRAKT(use_trakt):
def schedule_trakt(use_trakt):
if sickbeard.USE_TRAKT == use_trakt:
return
@ -216,22 +216,40 @@ def change_USE_TRAKT(use_trakt):
# pass
def change_USE_SUBTITLES(use_subtitles):
if sickbeard.USE_SUBTITLES == use_subtitles:
return
sickbeard.USE_SUBTITLES = use_subtitles
sickbeard.subtitlesFinderScheduler.check_paused()
def schedule_subtitles(use_subtitles):
if sickbeard.USE_SUBTITLES != use_subtitles:
sickbeard.USE_SUBTITLES = use_subtitles
sickbeard.subtitlesFinderScheduler.set_paused_state()
def CheckSection(CFG, sec):
def schedule_emby_watched(emby_watched_interval):
emby_watched_freq = minimax(emby_watched_interval, sickbeard.DEFAULT_WATCHEDSTATE_FREQUENCY,
0, sickbeard.MAX_WATCHEDSTATE_FREQUENCY)
if emby_watched_freq and emby_watched_freq != sickbeard.EMBY_WATCHEDSTATE_FREQUENCY:
sickbeard.EMBY_WATCHEDSTATE_FREQUENCY = emby_watched_freq
sickbeard.embyWatchedStateScheduler.cycleTime = datetime.timedelta(minutes=emby_watched_freq)
sickbeard.EMBY_WATCHEDSTATE_SCHEDULED = bool(emby_watched_freq)
sickbeard.embyWatchedStateScheduler.set_paused_state()
def schedule_plex_watched(plex_watched_interval):
plex_watched_freq = minimax(plex_watched_interval, sickbeard.DEFAULT_WATCHEDSTATE_FREQUENCY,
0, sickbeard.MAX_WATCHEDSTATE_FREQUENCY)
if plex_watched_freq and plex_watched_freq != sickbeard.PLEX_WATCHEDSTATE_FREQUENCY:
sickbeard.PLEX_WATCHEDSTATE_FREQUENCY = plex_watched_freq
sickbeard.plexWatchedStateScheduler.cycleTime = datetime.timedelta(minutes=plex_watched_freq)
sickbeard.PLEX_WATCHEDSTATE_SCHEDULED = bool(plex_watched_freq)
sickbeard.plexWatchedStateScheduler.set_paused_state()
def check_section(cfg, section):
""" Check if INI section exists, if not create it """
try:
CFG[sec]
return True
except:
CFG[sec] = {}
if section not in cfg:
cfg[section] = {}
return False
return True
def checkbox_to_value(option, value_on=1, value_off=0):
@ -284,7 +302,7 @@ def clean_host(host, default_port=None):
def clean_hosts(hosts, default_port=None):
cleaned_hosts = []
for cur_host in [x.strip() for x in hosts.split(',')]:
for cur_host in [host.strip() for host in hosts.split(',')]:
if cur_host:
cleaned_host = clean_host(cur_host, default_port)
if cleaned_host:
@ -329,7 +347,7 @@ def to_int(val, default=0):
try:
val = int(val)
except:
except(StandardError, Exception):
val = default
return val
@ -351,11 +369,11 @@ def minimax(val, default, low, high):
def check_setting_int(config, cfg_name, item_name, def_val):
try:
my_val = int(config[cfg_name][item_name])
except:
except(StandardError, Exception):
my_val = def_val
try:
config[cfg_name][item_name] = my_val
except:
except(StandardError, Exception):
config[cfg_name] = {}
config[cfg_name][item_name] = my_val
logger.log('%s -> %s' % (item_name, my_val), logger.DEBUG)
@ -365,11 +383,11 @@ def check_setting_int(config, cfg_name, item_name, def_val):
def check_setting_float(config, cfg_name, item_name, def_val):
try:
my_val = float(config[cfg_name][item_name])
except:
except(StandardError, Exception):
my_val = def_val
try:
config[cfg_name][item_name] = my_val
except:
except(StandardError, Exception):
config[cfg_name] = {}
config[cfg_name][item_name] = my_val
@ -391,11 +409,11 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True):
try:
my_val = helpers.decrypt(config[cfg_name][item_name], encryption_version)
except:
except(StandardError, Exception):
my_val = def_val
try:
config[cfg_name][item_name] = helpers.encrypt(my_val, encryption_version)
except:
except(StandardError, Exception):
config[cfg_name] = {}
config[cfg_name][item_name] = helpers.encrypt(my_val, encryption_version)
@ -407,7 +425,7 @@ def check_setting_str(config, cfg_name, item_name, def_val, log=True):
return (my_val, def_val)['None' == my_val]
class ConfigMigrator():
class ConfigMigrator:
def __init__(self, config_obj):
"""
Initializes a config migrator that can take the config from the version indicated in the config
@ -491,8 +509,8 @@ class ConfigMigrator():
sickbeard.NAMING_MULTI_EP = int(check_setting_int(self.config_obj, 'General', 'naming_multi_ep_type', 1))
# see if any of their shows used season folders
myDB = db.DBConnection()
season_folder_shows = myDB.select('SELECT * FROM tv_shows WHERE flatten_folders = 0')
my_db = db.DBConnection()
season_folder_shows = my_db.select('SELECT * FROM tv_shows WHERE flatten_folders = 0')
# if any shows had season folders on then prepend season folder to the pattern
if season_folder_shows:
@ -518,7 +536,7 @@ class ConfigMigrator():
logger.log(u'No shows were using season folders before so I am disabling flattening on all shows')
# don't flatten any shows at all
myDB.action('UPDATE tv_shows SET flatten_folders = 0')
my_db.action('UPDATE tv_shows SET flatten_folders = 0')
sickbeard.NAMING_FORCE_FOLDERS = naming.check_force_season_folders()
@ -534,11 +552,11 @@ class ConfigMigrator():
use_ep_name = bool(check_setting_int(self.config_obj, 'General', 'naming_ep_name', 1))
# make the presets into templates
naming_ep_type = ('%Sx%0E',
naming_ep_tmpl = ('%Sx%0E',
's%0Se%0E',
'S%0SE%0E',
'%0Sx%0E')
naming_sep_type = (' - ', ' ')
naming_sep_tmpl = (' - ', ' ')
# set up our data to use
if use_periods:
@ -555,29 +573,29 @@ class ConfigMigrator():
if abd:
ep_string = abd_string
else:
ep_string = naming_ep_type[ep_type]
ep_string = naming_ep_tmpl[ep_type]
finalName = ''
final_name = ''
# start with the show name
if use_show_name:
finalName += show_name + naming_sep_type[sep_type]
final_name += show_name + naming_sep_tmpl[sep_type]
# add the season/ep stuff
finalName += ep_string
final_name += ep_string
# add the episode name
if use_ep_name:
finalName += naming_sep_type[sep_type] + ep_name
final_name += naming_sep_tmpl[sep_type] + ep_name
# add the quality
if use_quality:
finalName += naming_sep_type[sep_type] + ep_quality
final_name += naming_sep_tmpl[sep_type] + ep_quality
if use_periods:
finalName = re.sub('\s+', '.', finalName)
final_name = re.sub('\s+', '.', final_name)
return finalName
return final_name
# Migration v2: Dummy migration to sync backup number with config version number
def _migrate_v2(self):
@ -589,7 +607,8 @@ class ConfigMigrator():
Reads in the old naming settings from your config and generates a new config template from them.
"""
# get the old settings from the file and store them in the new variable names
for prov in [curProvider for curProvider in sickbeard.providers.sortedProviderList() if curProvider.name == 'omgwtfnzbs']:
for prov in [curProvider for curProvider in sickbeard.providers.sortedProviderList()
if 'omgwtfnzbs' == curProvider.name]:
prov.username = check_setting_str(self.config_obj, 'omgwtfnzbs', 'omgwtfnzbs_uid', '')
prov.api_key = check_setting_str(self.config_obj, 'omgwtfnzbs', 'omgwtfnzbs_key', '')
@ -662,7 +681,7 @@ class ConfigMigrator():
use_banner = bool(check_setting_int(self.config_obj, 'General', 'use_banner', 0))
def _migrate_metadata(metadata, metadata_name, use_banner):
def _migrate_metadata(metadata, metadata_name, banner):
cur_metadata = metadata.split('|')
# if target has the old number of values, do upgrade
if len(cur_metadata) == 6:
@ -673,9 +692,9 @@ class ConfigMigrator():
cur_metadata.append('0')
# swap show fanart, show poster
cur_metadata[3], cur_metadata[2] = cur_metadata[2], cur_metadata[3]
# if user was using use_banner to override the poster,
# if user was using banner to override the poster,
# instead enable the banner option and deactivate poster
if metadata_name == 'XBMC' and use_banner:
if 'XBMC' == metadata_name and banner:
cur_metadata[4], cur_metadata[3] = cur_metadata[3], '0'
# write new format
metadata = '|'.join(cur_metadata)
@ -723,7 +742,8 @@ class ConfigMigrator():
check_setting_int(self.config_obj, 'GUI', 'coming_eps_display_paused', 0))
sickbeard.EPISODE_VIEW_MISSED_RANGE = check_setting_int(self.config_obj, 'GUI', 'coming_eps_missed_range', 7)
def _migrate_v8(self):
@staticmethod
def _migrate_v8():
# removing settings from gui and making it a hidden debug option
sickbeard.RECENTSEARCH_STARTUP = False
@ -731,7 +751,8 @@ class ConfigMigrator():
sickbeard.PUSHBULLET_ACCESS_TOKEN = check_setting_str(self.config_obj, 'Pushbullet', 'pushbullet_api', '')
sickbeard.PUSHBULLET_DEVICE_IDEN = check_setting_str(self.config_obj, 'Pushbullet', 'pushbullet_device', '')
def _migrate_v10(self):
@staticmethod
def _migrate_v10():
# reset backlog frequency to default
sickbeard.BACKLOG_FREQUENCY = sickbeard.DEFAULT_BACKLOG_FREQUENCY
@ -741,9 +762,11 @@ class ConfigMigrator():
else:
sickbeard.SHOWLIST_TAGVIEW = 'default'
def _migrate_v12(self):
@staticmethod
def _migrate_v12():
# add words to ignore list and insert spaces to improve the ui config readability
words_to_add = ['hevc', 'reenc', 'x265', 'danish', 'deutsch', 'flemish', 'italian', 'nordic', 'norwegian', 'portuguese', 'spanish', 'turkish']
words_to_add = ['hevc', 'reenc', 'x265', 'danish', 'deutsch', 'flemish', 'italian',
'nordic', 'norwegian', 'portuguese', 'spanish', 'turkish']
config_words = sickbeard.IGNORE_WORDS.split(',')
new_list = []
for new_word in words_to_add:
@ -759,7 +782,8 @@ class ConfigMigrator():
sickbeard.IGNORE_WORDS = ', '.join(sorted(new_list))
def _migrate_v13(self):
@staticmethod
def _migrate_v13():
# change dereferrer.org urls to blank, but leave any other url untouched
if sickbeard.ANON_REDIRECT == 'http://dereferer.org/?':
sickbeard.ANON_REDIRECT = ''

View file

@ -27,7 +27,7 @@ from sickbeard import encodingKludge as ek
from sickbeard.name_parser.parser import NameParser, InvalidNameException, InvalidShowException
MIN_DB_VERSION = 9 # oldest db version we support migrating from
MAX_DB_VERSION = 20008
MAX_DB_VERSION = 20009
TEST_BASE_VERSION = None # the base production db version, only needed for TEST db versions (>=100000)
@ -44,12 +44,13 @@ class MainSanityCheck(db.DBSanityCheck):
def fix_duplicate_shows(self, column='indexer_id'):
sql_results = self.connection.select(
'SELECT show_id, ' + column + ', COUNT(' + column + ') as count FROM tv_shows GROUP BY ' + column + ' HAVING count > 1')
'SELECT show_id, %(col)s, COUNT(%(col)s) as count FROM tv_shows GROUP BY %(col)s HAVING count > 1'
% {'col': column})
for cur_duplicate in sql_results:
logger.log(u'Duplicate show detected! %s: %s count: %s' % (column, cur_duplicate[column],
cur_duplicate['count']), logger.DEBUG)
logger.log(u'Duplicate show detected! %s: %s count: %s' % (
column, cur_duplicate[column], cur_duplicate['count']), logger.DEBUG)
cur_dupe_results = self.connection.select(
'SELECT show_id, ' + column + ' FROM tv_shows WHERE ' + column + ' = ? LIMIT ?',
@ -58,9 +59,8 @@ class MainSanityCheck(db.DBSanityCheck):
cl = []
for cur_dupe_id in cur_dupe_results:
logger.log(
u'Deleting duplicate show with %s: %s show_id: %s' % (column, cur_dupe_id[column],
cur_dupe_id['show_id']))
logger.log(u'Deleting duplicate show with %s: %s show_id: %s' % (
column, cur_dupe_id[column], cur_dupe_id['show_id']))
cl.append(['DELETE FROM tv_shows WHERE show_id = ?', [cur_dupe_id['show_id']]])
if 0 < len(cl):
@ -72,7 +72,10 @@ class MainSanityCheck(db.DBSanityCheck):
def fix_duplicate_episodes(self):
sql_results = self.connection.select(
'SELECT showid, season, episode, COUNT(showid) as count FROM tv_episodes GROUP BY showid, season, episode HAVING count > 1')
'SELECT showid, season, episode, COUNT(showid) as count'
' FROM tv_episodes'
' GROUP BY showid, season, episode'
' HAVING count > 1')
for cur_duplicate in sql_results:
@ -81,7 +84,10 @@ class MainSanityCheck(db.DBSanityCheck):
cur_duplicate['count']), logger.DEBUG)
cur_dupe_results = self.connection.select(
'SELECT episode_id FROM tv_episodes WHERE showid = ? AND season = ? and episode = ? ORDER BY episode_id DESC LIMIT ?',
'SELECT episode_id'
' FROM tv_episodes'
' WHERE showid = ? AND season = ? and episode = ?'
' ORDER BY episode_id DESC LIMIT ?',
[cur_duplicate['showid'], cur_duplicate['season'], cur_duplicate['episode'],
int(cur_duplicate['count']) - 1]
)
@ -100,12 +106,15 @@ class MainSanityCheck(db.DBSanityCheck):
def fix_orphan_episodes(self):
sql_results = self.connection.select(
'SELECT episode_id, showid, tv_shows.indexer_id FROM tv_episodes LEFT JOIN tv_shows ON tv_episodes.showid=tv_shows.indexer_id WHERE tv_shows.indexer_id is NULL')
'SELECT episode_id, showid, tv_shows.indexer_id'
' FROM tv_episodes'
' LEFT JOIN tv_shows ON tv_episodes.showid=tv_shows.indexer_id'
' WHERE tv_shows.indexer_id is NULL')
cl = []
for cur_orphan in sql_results:
logger.log(u'Orphan episode detected! episode_id: %s showid: %s' % (cur_orphan['episode_id'],
cur_orphan['showid']), logger.DEBUG)
logger.log(u'Orphan episode detected! episode_id: %s showid: %s' % (
cur_orphan['episode_id'], cur_orphan['showid']), logger.DEBUG)
logger.log(u'Deleting orphan episode with episode_id: %s' % cur_orphan['episode_id'])
cl.append(['DELETE FROM tv_episodes WHERE episode_id = ?', [cur_orphan['episode_id']]])
@ -150,15 +159,15 @@ class MainSanityCheck(db.DBSanityCheck):
sql_results = self.connection.select(
'SELECT episode_id, showid FROM tv_episodes WHERE status = ? or ( airdate > ? AND status in (?,?) ) or '
'( airdate <= 1 AND status = ? )', ['', cur_date.toordinal(), common.SKIPPED, common.WANTED, common.WANTED])
'(airdate <= 1 AND status = ? )', ['', cur_date.toordinal(), common.SKIPPED, common.WANTED, common.WANTED])
cl = []
for cur_unaired in sql_results:
logger.log(u'UNAIRED episode detected! episode_id: %s showid: %s' % (cur_unaired['episode_id'],
cur_unaired['showid']), logger.DEBUG)
logger.log(u'UNAIRED episode detected! episode_id: %s showid: %s' % (
cur_unaired['episode_id'], cur_unaired['showid']), logger.DEBUG)
logger.log(u'Fixing unaired episode status with episode_id: %s' % cur_unaired['episode_id'])
cl.append(['UPDATE tv_episodes SET status = ? WHERE episode_id = ?',
[common.UNAIRED, cur_unaired['episode_id']]])
[common.UNAIRED, cur_unaired['episode_id']]])
if 0 < len(cl):
self.connection.mass_action(cl)
@ -176,42 +185,90 @@ class MainSanityCheck(db.DBSanityCheck):
self.connection.action('UPDATE scene_exceptions SET season = -1 WHERE season = "null"')
def fix_orphan_not_found_show(self):
sql_result = self.connection.action('DELETE FROM tv_shows_not_found WHERE NOT EXISTS (SELECT NULL FROM '
'tv_shows WHERE tv_shows_not_found.indexer == tv_shows.indexer AND '
'tv_shows_not_found.indexer_id == tv_shows.indexer_id)')
sql_result = self.connection.action(
'DELETE FROM tv_shows_not_found'
' WHERE NOT EXISTS (SELECT NULL FROM tv_shows WHERE tv_shows_not_found.indexer == tv_shows.indexer AND'
' tv_shows_not_found.indexer_id == tv_shows.indexer_id)')
if sql_result.rowcount:
logger.log('Fixed orphaned not found shows')
# ======================
# = Main DB Migrations =
# ======================
# Add new migrations at the bottom of the list; subclass the previous migration.
# 0 -> 20003
class InitialSchema(db.SchemaUpgrade):
# ======================
# = Main DB Migrations =
# ======================
# Add new migrations at the bottom of the list; subclass the previous migration.
# 0 -> 20007
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
if not self.hasTable('tv_shows') and not self.hasTable('db_version'):
queries = [
# original sick beard tables
'CREATE TABLE db_version (db_version INTEGER);',
'CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC, quality NUMERIC, resource TEXT, provider TEXT, version NUMERIC)',
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search NUMERIC)',
'CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, indexerid NUMERIC, indexer NUMERIC, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC, hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT, file_size NUMERIC, release_name TEXT, subtitles TEXT, subtitles_searchcount NUMERIC, subtitles_lastsearch TIMESTAMP, is_proper NUMERIC, scene_season NUMERIC, scene_episode NUMERIC, absolute_number NUMERIC, scene_absolute_number NUMERIC, version NUMERIC, release_group TEXT, trakt_watched NUMERIC)',
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, indexer_id NUMERIC, indexer NUMERIC, show_name TEXT, location TEXT, network TEXT, genre TEXT, classification TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, air_by_date NUMERIC, lang TEXT, subtitles NUMERIC, notify_list TEXT, imdb_id TEXT, last_update_indexer NUMERIC, dvdorder NUMERIC, archive_firstmatch NUMERIC, rls_require_words TEXT, rls_ignore_words TEXT, sports NUMERIC, anime NUMERIC, scene NUMERIC, overview TEXT, tag TEXT)',
'CREATE INDEX idx_showid ON tv_episodes (showid)',
'CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes (showid,airdate)',
# blacklist
'CREATE TABLE blacklist (show_id INTEGER, range TEXT, keyword TEXT)',
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER, mindexer NUMERIC, PRIMARY KEY (indexer_id, indexer))',
'CREATE TABLE imdb_info (indexer_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)',
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, absolute_number NUMERIC, scene_absolute_number NUMERIC, PRIMARY KEY (indexer_id, season, episode))',
'CREATE TABLE whitelist (show_id INTEGER, range TEXT, keyword TEXT)',
'CREATE TABLE xem_refresh (indexer TEXT, indexer_id INTEGER PRIMARY KEY, last_refreshed INTEGER)',
# db_version
'CREATE TABLE db_version (db_version INTEGER)',
'INSERT INTO db_version (db_version) VALUES (20008)',
# flags
'CREATE TABLE flags (flag PRIMARY KEY NOT NULL)',
# history
'CREATE TABLE history (action NUMERIC, date NUMERIC, showid NUMERIC, season NUMERIC, episode NUMERIC,'
' quality NUMERIC, resource TEXT, provider TEXT, version NUMERIC)',
# imdb_info
'CREATE TABLE imdb_info (indexer_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC,'
' akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT,'
' rating TEXT, votes INTEGER, last_update NUMERIC)',
# indexer_mapping
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER NOT NULL,'
' mindexer NUMERIC, date NUMERIC NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0,'
' PRIMARY KEY (indexer_id, indexer, mindexer))',
'CREATE INDEX idx_mapping ON indexer_mapping (indexer_id, indexer)',
# info
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search NUMERIC,'
' last_run_backlog NUMERIC NOT NULL DEFAULT 1)',
# scene_exceptions
'CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, indexer_id INTEGER KEY,'
' show_name TEXT, season NUMERIC, custom NUMERIC)',
# scene_exceptions_refresh
'CREATE TABLE scene_exceptions_refresh (list TEXT PRIMARY KEY, last_refreshed INTEGER)',
# scene_numbering
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER,'
' scene_season INTEGER, scene_episode INTEGER, absolute_number NUMERIC, scene_absolute_number NUMERIC,'
' PRIMARY KEY (indexer_id, season, episode))',
# tv_episodes
'CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, indexerid NUMERIC,'
' indexer NUMERIC, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC,'
' hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT, file_size NUMERIC, release_name TEXT,'
' subtitles TEXT, subtitles_searchcount NUMERIC, subtitles_lastsearch TIMESTAMP, is_proper NUMERIC,'
' scene_season NUMERIC, scene_episode NUMERIC, absolute_number NUMERIC, scene_absolute_number NUMERIC,'
' version NUMERIC, release_group TEXT, trakt_watched NUMERIC)',
'CREATE INDEX idx_showid ON tv_episodes (showid)',
'CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes (showid, airdate)',
'CREATE INDEX idx_sta_epi_air ON tv_episodes (status, episode, airdate)',
'CREATE INDEX idx_sta_epi_sta_air ON tv_episodes (season, episode, status, airdate)',
'CREATE INDEX idx_status ON tv_episodes (status, season, episode, airdate)',
# tv_episodes_watched
'CREATE TABLE tv_episodes_watched (tvep_id NUMERIC NOT NULL, clientep_id TEXT, label TEXT,'
' played NUMERIC DEFAULT 0 NOT NULL, date_watched NUMERIC NOT NULL, date_added NUMERIC,'
' status NUMERIC, location TEXT, file_size NUMERIC, hide INT default 0 not null)',
# tv_shows
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, indexer_id NUMERIC, indexer NUMERIC,'
' show_name TEXT, location TEXT, network TEXT, genre TEXT, classification TEXT, runtime NUMERIC,'
' quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC,'
' air_by_date NUMERIC, lang TEXT, subtitles NUMERIC, notify_list TEXT, imdb_id TEXT,'
' last_update_indexer NUMERIC, dvdorder NUMERIC, archive_firstmatch NUMERIC, rls_require_words TEXT,'
' rls_ignore_words TEXT, sports NUMERIC, anime NUMERIC, scene NUMERIC, overview TEXT, tag TEXT)',
'CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id)',
'CREATE INDEX idx_sta_epi_air ON tv_episodes (status,episode, airdate)',
'CREATE INDEX idx_sta_epi_sta_air ON tv_episodes (season,episode, status, airdate)',
'CREATE INDEX idx_status ON tv_episodes (status,season,episode,airdate)',
'INSERT INTO db_version (db_version) VALUES (20003)'
# tv_shows_not_found
'CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL,'
' fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC,'
' PRIMARY KEY (indexer_id, indexer))',
# webdl_types
'CREATE TABLE webdl_types (dname TEXT NOT NULL, regex TEXT NOT NULL)',
# whitelist
'CREATE TABLE whitelist (show_id INTEGER, range TEXT, keyword TEXT)',
# xem_refresh
'CREATE TABLE xem_refresh (indexer TEXT, indexer_id INTEGER PRIMARY KEY, last_refreshed INTEGER)',
]
for query in queries:
self.connection.action(query)
@ -220,20 +277,22 @@ class InitialSchema(db.SchemaUpgrade):
cur_db_version = self.checkDBVersion()
if cur_db_version < MIN_DB_VERSION:
logger.log_error_and_exit(u'Your database version ('
+ str(cur_db_version)
+ ') is too old to migrate from what this version of SickGear supports ('
+ str(MIN_DB_VERSION) + ').' + "\n"
+ 'Upgrade using a previous version (tag) build 496 to build 501 of SickGear first or remove database file to begin fresh.'
)
logger.log_error_and_exit(
u'Your database version (' + str(cur_db_version)
+ ') is too old to migrate from what this version of SickGear supports ('
+ str(MIN_DB_VERSION) + ').' + "\n"
+ 'Upgrade using a previous version (tag) build 496 to build 501 of SickGear'
' first or remove database file to begin fresh.'
)
if cur_db_version > MAX_DB_VERSION:
logger.log_error_and_exit(u'Your database version ('
+ str(cur_db_version)
+ ') has been incremented past what this version of SickGear supports ('
+ str(MAX_DB_VERSION) + ').' + "\n"
+ 'If you have used other forks of SickGear, your database may be unusable due to their modifications.'
)
logger.log_error_and_exit(
u'Your database version (' + str(cur_db_version)
+ ') has been incremented past what this version of SickGear supports ('
+ str(MAX_DB_VERSION) + ').\n'
+ 'If you have used other forks of SickGear,'
' your database may be unusable due to their modifications.'
)
return self.checkDBVersion()
@ -270,7 +329,9 @@ class AddSizeAndSceneNameFields(db.SchemaUpgrade):
for cur_result in history_results:
# find the associated download, if there isn't one then ignore it
download_results = self.connection.select(
'SELECT resource FROM history WHERE provider = -1 AND showid = ? AND season = ? AND episode = ? AND date > ?',
'SELECT resource'
' FROM history'
' WHERE provider = -1 AND showid = ? AND season = ? AND episode = ? AND date > ?',
[cur_result['showid'], cur_result['season'], cur_result['episode'], cur_result['date']])
if not download_results:
logger.log(u'Found a snatch in the history for ' + cur_result[
@ -286,7 +347,9 @@ class AddSizeAndSceneNameFields(db.SchemaUpgrade):
# find the associated episode on disk
ep_results = self.connection.select(
'SELECT episode_id, status FROM tv_episodes WHERE showid = ? AND season = ? AND episode = ? AND location != ""',
'SELECT episode_id, status'
' FROM tv_episodes'
' WHERE showid = ? AND season = ? AND episode = ? AND location != ""',
[cur_result['showid'], cur_result['season'], cur_result['episode']])
if not ep_results:
logger.log(
@ -358,8 +421,14 @@ class RenameSeasonFolders(db.SchemaUpgrade):
# rename the column
self.connection.action('ALTER TABLE tv_shows RENAME TO tmp_tv_shows')
self.connection.action(
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, location TEXT, show_name TEXT, tvdb_id NUMERIC, network TEXT, genre TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, tvr_id NUMERIC, tvr_name TEXT, air_by_date NUMERIC, lang TEXT)')
sql = 'INSERT INTO tv_shows(show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, tvr_id, tvr_name, air_by_date, lang) SELECT show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status, seasonfolders, paused, startyear, tvr_id, tvr_name, air_by_date, lang FROM tmp_tv_shows'
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, location TEXT, show_name TEXT, tvdb_id NUMERIC,'
' network TEXT, genre TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT,'
' flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, tvr_id NUMERIC, tvr_name TEXT,'
' air_by_date NUMERIC, lang TEXT)')
sql = 'INSERT INTO tv_shows(show_id, location, show_name, tvdb_id, network, genre, runtime,' \
' quality, airs, status, flatten_folders, paused, startyear, tvr_id, tvr_name, air_by_date, lang)' \
' SELECT show_id, location, show_name, tvdb_id, network, genre, runtime, quality, airs, status,' \
' seasonfolders, paused, startyear, tvr_id, tvr_name, air_by_date, lang FROM tmp_tv_shows'
self.connection.action(sql)
# flip the values to be opposite of what they were before
@ -419,13 +488,13 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
return result
def _update_composite_qualities(self, status):
'''
"""
Unpack, Update, Return new quality values
Unpack the composite archive/initial values.
Update either qualities if needed.
Then return the new compsite quality value.
'''
"""
best = (status & (0xffff << 16)) >> 16
initial = status & 0xffff
@ -449,7 +518,8 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
new_hd = common.Quality.combineQualities([common.Quality.HDTV, common.Quality.HDWEBDL,
common.Quality.HDBLURAY], [])
# update ANY -- shift existing qualities and add new 1080p qualities, note that rawHD was not added to the ANY template
# update ANY -- shift existing qualities and add new 1080p qualities,
# note that rawHD was not added to the ANY template
old_any = common.Quality.combineQualities(
[common.Quality.SDTV, common.Quality.SDDVD, common.Quality.HDTV, common.Quality.HDWEBDL >> 2,
common.Quality.HDBLURAY >> 3, common.Quality.UNKNOWN], [])
@ -472,7 +542,8 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
cl.append(['UPDATE tv_shows SET quality = ? WHERE show_id = ?', [new_quality, cur_show['show_id']]])
self.connection.mass_action(cl)
# update status that are are within the old hdwebdl (1<<3 which is 8) and better -- exclude unknown (1<<15 which is 32768)
# update status that are are within the old hdwebdl
# (1<<3 which is 8) and better -- exclude unknown (1<<15 which is 32768)
logger.log(u'[2/4] Updating the status for the episodes within each show...', logger.MESSAGE)
cl = []
episodes = self.connection.select('SELECT * FROM tv_episodes WHERE status < 3276800 AND status >= 800')
@ -481,7 +552,8 @@ class Add1080pAndRawHDQualities(db.SchemaUpgrade):
[self._update_status(cur_episode['status']), cur_episode['episode_id']]])
self.connection.mass_action(cl)
# make two seperate passes through the history since snatched and downloaded (action & quality) may not always coordinate together
# make two seperate passes through the history since snatched and downloaded (action & quality)
# may not always coordinate together
# update previous history so it shows the correct action
logger.log(u'[3/4] Updating history to reflect the correct action...', logger.MESSAGE)
@ -564,7 +636,9 @@ class AddIMDbInfo(db.SchemaUpgrade):
db.backup_database('sickbeard.db', self.checkDBVersion())
db_backed_up = True
self.connection.action(
'CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)')
'CREATE TABLE imdb_info (tvdb_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC,'
' akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT,'
' rating TEXT, votes INTEGER, last_update NUMERIC)')
if not self.hasColumn('tv_shows', 'imdb_id'):
logger.log(u'Adding IMDb column imdb_id to tv_shows')
@ -680,9 +754,18 @@ class ConvertTVShowsToIndexerScheme(db.SchemaUpgrade):
self.connection.action('ALTER TABLE tv_shows RENAME TO tmp_tv_shows')
self.connection.action(
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, indexer_id NUMERIC, indexer NUMERIC, show_name TEXT, location TEXT, network TEXT, genre TEXT, classification TEXT, runtime NUMERIC, quality NUMERIC, airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, air_by_date NUMERIC, lang TEXT, subtitles NUMERIC, notify_list TEXT, imdb_id TEXT, last_update_indexer NUMERIC, dvdorder NUMERIC)')
'CREATE TABLE tv_shows (show_id INTEGER PRIMARY KEY, indexer_id NUMERIC, indexer NUMERIC, show_name TEXT,'
' location TEXT, network TEXT, genre TEXT, classification TEXT, runtime NUMERIC, quality NUMERIC,'
' airs TEXT, status TEXT, flatten_folders NUMERIC, paused NUMERIC, startyear NUMERIC, air_by_date NUMERIC,'
' lang TEXT, subtitles NUMERIC, notify_list TEXT, imdb_id TEXT,'
' last_update_indexer NUMERIC, dvdorder NUMERIC)')
self.connection.action(
'INSERT INTO tv_shows(show_id, indexer_id, show_name, location, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list, imdb_id, last_update_indexer, dvdorder) SELECT show_id, tvdb_id, show_name, location, network, genre, runtime, quality, airs, status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list, imdb_id, last_update_tvdb, dvdorder FROM tmp_tv_shows')
'INSERT INTO tv_shows(show_id, indexer_id, show_name, location, network, genre, runtime, quality, airs,'
' status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list, imdb_id,'
' last_update_indexer, dvdorder)'
' SELECT show_id, tvdb_id, show_name, location, network, genre, runtime,'
' quality, airs, status, flatten_folders, paused, startyear, air_by_date, lang, subtitles, notify_list,'
' imdb_id, last_update_tvdb, dvdorder FROM tmp_tv_shows')
self.connection.action('DROP TABLE tmp_tv_shows')
self.connection.action('CREATE UNIQUE INDEX idx_indexer_id ON tv_shows (indexer_id);')
@ -707,9 +790,16 @@ class ConvertTVEpisodesToIndexerScheme(db.SchemaUpgrade):
self.connection.action('ALTER TABLE tv_episodes RENAME TO tmp_tv_episodes')
self.connection.action(
'CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, indexerid NUMERIC, indexer NUMERIC, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC, hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT, file_size NUMERIC, release_name TEXT, subtitles TEXT, subtitles_searchcount NUMERIC, subtitles_lastsearch TIMESTAMP, is_proper NUMERIC)')
'CREATE TABLE tv_episodes (episode_id INTEGER PRIMARY KEY, showid NUMERIC, indexerid NUMERIC,'
' indexer NUMERIC, name TEXT, season NUMERIC, episode NUMERIC, description TEXT, airdate NUMERIC,'
' hasnfo NUMERIC, hastbn NUMERIC, status NUMERIC, location TEXT, file_size NUMERIC, release_name TEXT,'
' subtitles TEXT, subtitles_searchcount NUMERIC, subtitles_lastsearch TIMESTAMP, is_proper NUMERIC)')
self.connection.action(
'INSERT INTO tv_episodes(episode_id, showid, indexerid, name, season, episode, description, airdate, hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount, subtitles_lastsearch, is_proper) SELECT episode_id, showid, tvdbid, name, season, episode, description, airdate, hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount, subtitles_lastsearch, is_proper FROM tmp_tv_episodes')
'INSERT INTO tv_episodes(episode_id, showid, indexerid, name, season, episode, description, airdate,'
' hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount,'
' subtitles_lastsearch, is_proper) SELECT episode_id, showid, tvdbid, name, season, episode, description,'
' airdate, hasnfo, hastbn, status, location, file_size, release_name, subtitles, subtitles_searchcount,'
' subtitles_lastsearch, is_proper FROM tmp_tv_episodes')
self.connection.action('DROP TABLE tmp_tv_episodes')
self.connection.action('CREATE INDEX idx_tv_episodes_showid_airdate ON tv_episodes(showid,airdate);')
@ -737,9 +827,13 @@ class ConvertIMDBInfoToIndexerScheme(db.SchemaUpgrade):
self.connection.action('ALTER TABLE imdb_info RENAME TO tmp_imdb_info')
self.connection.action(
'CREATE TABLE imdb_info (indexer_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT, runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT, votes INTEGER, last_update NUMERIC)')
'CREATE TABLE imdb_info (indexer_id INTEGER PRIMARY KEY, imdb_id TEXT, title TEXT, year NUMERIC, akas TEXT,'
' runtimes NUMERIC, genres TEXT, countries TEXT, country_codes TEXT, certificates TEXT, rating TEXT,'
' votes INTEGER, last_update NUMERIC)')
self.connection.action(
'INSERT INTO imdb_info(indexer_id, imdb_id, title, year, akas, runtimes, genres, countries, country_codes, certificates, rating, votes, last_update) SELECT tvdb_id, imdb_id, title, year, akas, runtimes, genres, countries, country_codes, certificates, rating, votes, last_update FROM tmp_imdb_info')
'INSERT INTO imdb_info(indexer_id, imdb_id, title, year, akas, runtimes, genres, countries, country_codes,'
' certificates, rating, votes, last_update) SELECT tvdb_id, imdb_id, title, year, akas, runtimes, genres,'
' countries, country_codes, certificates, rating, votes, last_update FROM tmp_imdb_info')
self.connection.action('DROP TABLE tmp_imdb_info')
self.incDBVersion()
@ -761,7 +855,8 @@ class ConvertInfoToIndexerScheme(db.SchemaUpgrade):
self.connection.action(
'CREATE TABLE info (last_backlog NUMERIC, last_indexer NUMERIC, last_proper_search NUMERIC)')
self.connection.action(
'INSERT INTO info(last_backlog, last_indexer, last_proper_search) SELECT last_backlog, last_tvdb, last_proper_search FROM tmp_info')
'INSERT INTO info(last_backlog, last_indexer, last_proper_search)'
' SELECT last_backlog, last_tvdb, last_proper_search FROM tmp_info')
self.connection.action('DROP TABLE tmp_info')
self.incDBVersion()
@ -791,7 +886,9 @@ class AddSceneNumbering(db.SchemaUpgrade):
logger.log(u'Upgrading table scene_numbering ...', logger.MESSAGE)
self.connection.action(
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER, scene_season INTEGER, scene_episode INTEGER, PRIMARY KEY (indexer_id,season,episode))')
'CREATE TABLE scene_numbering (indexer TEXT, indexer_id INTEGER, season INTEGER, episode INTEGER,'
' scene_season INTEGER, scene_episode INTEGER,'
' PRIMARY KEY (indexer_id,season,episode))')
self.incDBVersion()
return self.checkDBVersion()
@ -981,7 +1078,8 @@ class AddIndexerMapping(db.SchemaUpgrade):
logger.log(u'Adding table indexer_mapping')
self.connection.action(
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER, mindexer NUMERIC, PRIMARY KEY (indexer_id, indexer))')
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER, mindexer NUMERIC,'
' PRIMARY KEY (indexer_id, indexer))')
self.incDBVersion()
return self.checkDBVersion()
@ -1190,7 +1288,9 @@ class ChangeMapIndexer(db.SchemaUpgrade):
logger.log(u'Changing table indexer_mapping')
self.connection.action(
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER NOT NULL, mindexer NUMERIC, date NUMERIC NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (indexer_id, indexer, mindexer))')
'CREATE TABLE indexer_mapping (indexer_id INTEGER, indexer NUMERIC, mindexer_id INTEGER NOT NULL,'
' mindexer NUMERIC, date NUMERIC NOT NULL DEFAULT 0, status INTEGER NOT NULL DEFAULT 0,'
' PRIMARY KEY (indexer_id, indexer, mindexer))')
self.connection.action('CREATE INDEX IF NOT EXISTS idx_mapping ON indexer_mapping (indexer_id, indexer)')
@ -1204,21 +1304,23 @@ class ChangeMapIndexer(db.SchemaUpgrade):
self.connection.action('CREATE TABLE scene_exceptions_refresh (list TEXT PRIMARY KEY, last_refreshed INTEGER)')
if self.hasTable('scene_exceptions'):
self.connection.action('DROP TABLE scene_exceptions')
self.connection.action('CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY, indexer_id INTEGER KEY, show_name TEXT, season NUMERIC, custom NUMERIC)')
self.connection.action('CREATE TABLE scene_exceptions (exception_id INTEGER PRIMARY KEY,'
' indexer_id INTEGER KEY, show_name TEXT, season NUMERIC, custom NUMERIC)')
try:
cachedb = db.DBConnection(filename='cache.db')
if cachedb.hasTable('scene_exceptions'):
sqlResults = cachedb.action('SELECT * FROM scene_exceptions')
sql_results = cachedb.action('SELECT * FROM scene_exceptions')
cs = []
for r in sqlResults:
cs.append(['INSERT OR REPLACE INTO scene_exceptions (exception_id, indexer_id, show_name, season, custom)'
' VALUES (?,?,?,?,?)', [r['exception_id'], r['indexer_id'], r['show_name'],
r['season'], r['custom']]])
for r in sql_results:
cs.append(
['INSERT OR REPLACE INTO scene_exceptions (exception_id, indexer_id, show_name, season, custom)'
' VALUES (?,?,?,?,?)',
[r['exception_id'], r['indexer_id'], r['show_name'], r['season'], r['custom']]])
if len(cs) > 0:
if 0 < len(cs):
self.connection.mass_action(cs)
except:
except (StandardError, Exception):
pass
keep_tables = {'scene_exceptions', 'scene_exceptions_refresh', 'info', 'indexer_mapping', 'blacklist',
@ -1242,7 +1344,10 @@ class AddShowNotFoundCounter(db.SchemaUpgrade):
logger.log(u'Adding table tv_shows_not_found')
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action('CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL, fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC, PRIMARY KEY (indexer_id, indexer))')
self.connection.action(
'CREATE TABLE tv_shows_not_found (indexer NUMERIC NOT NULL, indexer_id NUMERIC NOT NULL,'
' fail_count NUMERIC NOT NULL DEFAULT 0, last_check NUMERIC NOT NULL, last_success NUMERIC,'
' PRIMARY KEY (indexer_id, indexer))')
self.setDBVersion(20005)
return self.checkDBVersion()
@ -1255,7 +1360,7 @@ class AddFlagTable(db.SchemaUpgrade):
logger.log(u'Adding table flags')
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action('CREATE TABLE flags (flag PRIMARY KEY NOT NULL )')
self.connection.action('CREATE TABLE flags (flag PRIMARY KEY NOT NULL)')
self.setDBVersion(20006)
return self.checkDBVersion()
@ -1275,7 +1380,29 @@ class DBIncreaseTo20007(db.SchemaUpgrade):
class AddWebdlTypesTable(db.SchemaUpgrade):
def execute(self):
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action('CREATE TABLE webdl_types (dname TEXT NOT NULL , regex TEXT NOT NULL )')
self.connection.action('CREATE TABLE webdl_types (dname TEXT NOT NULL, regex TEXT NOT NULL)')
self.setDBVersion(20008)
return self.checkDBVersion()
# 20008 -> 20009
class AddWatched(db.SchemaUpgrade):
def execute(self):
# remove old table from version 20007
if self.hasTable('tv_episodes_watched') and not self.hasColumn('tv_episodes_watched', 'clientep_id'):
self.connection.action('DROP TABLE tv_episodes_watched')
self.connection.action('VACUUM')
if not self.hasTable('tv_episodes_watched'):
logger.log(u'Adding table tv_episodes_watched')
db.backup_database('sickbeard.db', self.checkDBVersion())
self.connection.action(
'CREATE TABLE tv_episodes_watched (tvep_id NUMERIC NOT NULL, clientep_id TEXT, label TEXT,'
' played NUMERIC DEFAULT 0 NOT NULL, date_watched NUMERIC NOT NULL, date_added NUMERIC,'
' status NUMERIC, location TEXT, file_size NUMERIC, hide INT default 0 not null)'
)
self.setDBVersion(20009)
return self.checkDBVersion()

View file

@ -47,26 +47,35 @@ def dbFilename(filename='sickbeard.db', suffix=None):
return ek.ek(os.path.join, sickbeard.DATA_DIR, filename)
def mass_upsert_sql(tableName, valueDict, keyDict):
def mass_upsert_sql(table_name, value_dict, key_dict, sanitise=True):
"""
use with cl.extend(mass_upsert_sql(tableName, valueDict, keyDict))
:param tableName: table name
:param valueDict: dict of values to be set {'table_fieldname': value}
:param keyDict: dict of restrains for update {'table_fieldname': value}
:param table_name: table name
:param value_dict: dict of values to be set {'table_fieldname': value}
:param key_dict: dict of restrains for update {'table_fieldname': value}
:param sanitise: True to remove k, v pairs in keyDict from valueDict as they must not exist in both.
This option has a performance hit so it's best to remove key_dict keys from value_dict and set this False instead.
:type sanitise: Boolean
:return: list of 2 sql command
"""
cl = []
genParams = lambda myDict: [x + ' = ?' for x in myDict.keys()]
gen_params = (lambda my_dict: [x + ' = ?' for x in my_dict.keys()])
cl.append(['UPDATE [%s] SET %s WHERE %s' % (
tableName, ', '.join(genParams(valueDict)), ' AND '.join(genParams(keyDict))), valueDict.values() + keyDict.values()])
# sanity: remove k, v pairs in keyDict from valueDict
if sanitise:
value_dict = dict(filter(lambda (k, _): k not in key_dict.keys(), value_dict.items()))
cl.append(['UPDATE [%s] SET %s WHERE %s' %
(table_name, ', '.join(gen_params(value_dict)), ' AND '.join(gen_params(key_dict))),
value_dict.values() + key_dict.values()])
cl.append(['INSERT INTO [' + tableName + '] (' + ', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in valueDict.keys() + keyDict.keys()]) + ')' +
' SELECT ' + ', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in valueDict.values() + keyDict.values()]) + ' WHERE changes() = 0'])
cl.append(['INSERT INTO [' + table_name + '] (' +
', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in value_dict.keys() + key_dict.keys()]) + ')' +
' SELECT ' +
', '.join(["'%s'" % ('%s' % v).replace("'", "''") for v in value_dict.values() + key_dict.values()]) +
' WHERE changes() = 0'])
return cl
@ -261,12 +270,41 @@ class DBConnection(object):
return False
def add_flag(self, flag_name):
if not self.has_flag(flag_name):
has_flag = self.has_flag(flag_name)
if not has_flag:
self.action('INSERT INTO flags (flag) VALUES (?)', [flag_name])
return not has_flag
def remove_flag(self, flag_name):
if self.has_flag(flag_name):
has_flag = self.has_flag(flag_name)
if has_flag:
self.action('DELETE FROM flags WHERE flag = ?', [flag_name])
return has_flag
def toggle_flag(self, flag_name):
"""
Add or remove a flag
:param flag_name: Name of flag
:type flag_name: String
:return: True if this call added the flag, False if flag is removed
:rtype: Boolean
"""
if self.remove_flag(flag_name):
return False
self.add_flag(flag_name)
return True
def set_flag(self, flag_name, state=True):
"""
Set state of flag
:param flag_name: Name of flag
:type flag_name: String
:param state: If true, create flag otherwise remove flag
:type state: Boolean
:return: Previous state of flag
:rtype: Boolean
"""
return (self.add_flag, self.remove_flag)[not bool(state)](flag_name)
def close(self):
"""Close database connection"""
@ -515,6 +553,7 @@ def MigrationCode(myDB):
20005: sickbeard.mainDB.AddFlagTable,
20006: sickbeard.mainDB.DBIncreaseTo20007,
20007: sickbeard.mainDB.AddWebdlTypesTable,
20008: sickbeard.mainDB.AddWatched,
# 20002: sickbeard.mainDB.AddCoolSickGearFeature3,
}
@ -532,6 +571,9 @@ def MigrationCode(myDB):
else:
while db_version < sickbeard.mainDB.MAX_DB_VERSION:
if None is schema[db_version]: # skip placeholders used when multi PRs are updating DB
db_version += 1
continue
try:
update = schema[db_version](myDB)
db_version = update.execute()

View file

@ -122,7 +122,7 @@ class EmbyNotifier(Notifier):
else:
new_keys += [key]
apikeys = (new_keys, [x.strip() for x in sickbeard.EMBY_APIKEY.split(',') if x.strip()] + new_keys)[has_old_key]
apikeys = has_old_key and [x.strip() for x in sickbeard.EMBY_APIKEY.split(',') if x.strip()] or [] + new_keys
if len(hosts) != len(apikeys):
message = ('Not enough Api keys for hosts', 'More Api keys than hosts')[len(apikeys) > len(hosts)]
@ -178,5 +178,10 @@ class EmbyNotifier(Notifier):
def discover_server(self):
return self._discover_server()
def check_config(self, hosts=None, apikeys=None):
self._testing = True # ensure _choose() uses passed args
return self._check_config(hosts, apikeys)
notifier = EmbyNotifier

View file

@ -100,7 +100,8 @@ class ProviderFailList(object):
fail_hour = e.fail_time.time().hour
date_time = datetime.datetime.combine(fail_date, datetime.time(hour=fail_hour))
if ProviderFailTypes.names[e.fail_type] not in fail_dict.get(date_time, {}):
default = {'date': str(fail_date), 'date_time': date_time, 'multirow': False}
default = {'date': str(fail_date), 'date_time': date_time,
'timestamp': helpers.tryInt(sbdatetime.totimestamp(e.fail_time)), 'multirow': False}
for et in ProviderFailTypes.names.itervalues():
default[et] = b_d.copy()
fail_dict.setdefault(date_time, default)[ProviderFailTypes.names[e.fail_type]]['count'] = 1
@ -502,8 +503,10 @@ class GenericProvider(object):
kwargs['raise_exceptions'] = True
kwargs['raise_status_code'] = True
for k, v in dict(headers=self.headers, hooks=dict(response=self.cb_response), session=self.session).items():
for k, v in dict(headers=self.headers, hooks=dict(response=self.cb_response)).items():
kwargs.setdefault(k, v)
if 'nzbs.in' not in url: # this provider returns 503's 3 out of 4 requests with the persistent session system
kwargs.setdefault('session', self.session)
post_data = kwargs.get('post_data')
post_json = kwargs.get('post_json')

View file

@ -55,14 +55,13 @@ class Scheduler(threading.Thread):
self._stop.set()
self.unpause()
def check_paused(self):
if hasattr(self.action, 'check_paused'):
if self.action.check_paused():
def set_paused_state(self):
if hasattr(self.action, 'is_enabled'):
self.silent = not self.action.is_enabled()
if self.silent:
self.pause()
self.silent = True
else:
self.unpause()
self.silent = False
def timeLeft(self):
return self.cycleTime - (datetime.datetime.now() - self.lastRun)
@ -74,55 +73,60 @@ class Scheduler(threading.Thread):
return False
def run(self):
self.check_paused()
self.set_paused_state()
# if self._unpause Event() is NOT set the loop pauses
while self._unpause.wait() and not self._stop.is_set():
try:
current_time = datetime.datetime.now()
should_run = False
# check if interval has passed
if current_time - self.lastRun >= self.cycleTime:
# check if wanting to start around certain time taking interval into account
if self.start_time:
hour_diff = current_time.time().hour - self.start_time.hour
if not hour_diff < 0 and hour_diff < self.cycleTime.seconds / 3600:
should_run = True
else:
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
else:
should_run = True
if self.force:
should_run = True
if should_run and self.prevent_cycle_run is not None and self.prevent_cycle_run():
logger.log(u'%s skipping this cycleTime' % self.name, logger.WARNING)
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
if getattr(self.action, 'is_enabled', True):
try:
current_time = datetime.datetime.now()
should_run = False
if should_run:
self.lastRun = current_time
# check if interval has passed
if current_time - self.lastRun >= self.cycleTime:
# check if wanting to start around certain time taking interval into account
if self.start_time:
hour_diff = current_time.time().hour - self.start_time.hour
if not hour_diff < 0 and hour_diff < self.cycleTime.seconds / 3600:
should_run = True
else:
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
else:
should_run = True
try:
if not self.silent:
logger.log(u"Starting new thread: " + self.name, logger.DEBUG)
if self.force:
should_run = True
self.action.run()
except Exception as e:
logger.log(u"Exception generated in thread " + self.name + ": " + ex(e), logger.ERROR)
logger.log(repr(traceback.format_exc()), logger.ERROR)
if should_run and ((self.prevent_cycle_run is not None and self.prevent_cycle_run()) or
getattr(self.action, 'prevent_run', False)):
logger.log(u'%s skipping this cycleTime' % self.name, logger.WARNING)
# set lastRun to only check start_time after another cycleTime
self.lastRun = current_time
should_run = False
finally:
if self.force:
self.force = False
if should_run:
self.lastRun = current_time
try:
if not self.silent:
logger.log(u"Starting new thread: " + self.name, logger.DEBUG)
self.action.run()
except Exception as e:
logger.log(u"Exception generated in thread " + self.name + ": " + ex(e), logger.ERROR)
logger.log(repr(traceback.format_exc()), logger.ERROR)
finally:
if self.force:
self.force = False
else:
# disabled schedulers will only be rechecked every 30 seconds until enabled
time.sleep(30)
time.sleep(1)
# exiting thread
self._stop.clear()
self._unpause.clear()
self._unpause.clear()

View file

@ -28,10 +28,8 @@ class ProperSearcher:
self.amActive = False
@staticmethod
def check_paused():
if sickbeard.DOWNLOAD_PROPERS:
return False
return True
def is_enabled():
return sickbeard.DOWNLOAD_PROPERS
def run(self):

View file

@ -49,10 +49,10 @@ def sortedServiceList():
newList.append(curServiceDict)
return newList
def getEnabledServiceList():
return [x['name'] for x in sortedServiceList() if x['enabled']]
def isValidLanguage(language):
return subliminal.language.language_list(language)
@ -81,18 +81,26 @@ def subtitlesLanguages(video_path):
def subtitleLanguageFilter():
return [language for language in subliminal.language.LANGUAGES if language[2] != ""]
class SubtitlesFinder():
class SubtitlesFinder:
"""
The SubtitlesFinder will be executed every hour but will not necessarly search
and download subtitles. Only if the defined rule is true
"""
@staticmethod
def check_paused():
if sickbeard.USE_SUBTITLES:
return False
return True
def __init__(self):
self.amActive = False
def run(self, force=False):
@staticmethod
def is_enabled():
return sickbeard.USE_SUBTITLES
def run(self):
if self.is_enabled():
self.amActive = True
self._main()
self.amActive = False
def _main(self):
if len(sickbeard.subtitles.getEnabledServiceList()) < 1:
logger.log(u'Not enough services selected. At least 1 service is required to search subtitles in the background', logger.ERROR)
return
@ -100,7 +108,7 @@ class SubtitlesFinder():
logger.log(u'Checking for subtitles', logger.MESSAGE)
# get episodes on which we want subtitles
# criteria is:
# criteria is:
# - show subtitles = 1
# - episode subtitles != config wanted languages or SINGLE (depends on config multi)
# - search count < 2 and diff(airdate, now) > 1 week : now -> 1d
@ -114,7 +122,7 @@ class SubtitlesFinder():
if len(sqlResults) == 0:
logger.log('No subtitles to download', logger.MESSAGE)
return
rules = self._getRules()
now = datetime.datetime.now()
for epToSub in sqlResults:
@ -122,26 +130,26 @@ class SubtitlesFinder():
if not ek.ek(os.path.isfile, epToSub['location']):
logger.log('Episode file does not exist, cannot download subtitles for episode %dx%d of show %s' % (epToSub['season'], epToSub['episode'], epToSub['show_name']), logger.DEBUG)
continue
# Old shows rule
throwaway = datetime.datetime.strptime('20110101', '%Y%m%d')
if ((epToSub['airdate_daydiff'] > 7 and epToSub['searchcount'] < 2 and now - datetime.datetime.strptime(epToSub['lastsearch'], '%Y-%m-%d %H:%M:%S') > datetime.timedelta(hours=rules['old'][epToSub['searchcount']])) or
# Recent shows rule
# Recent shows rule
(epToSub['airdate_daydiff'] <= 7 and epToSub['searchcount'] < 7 and now - datetime.datetime.strptime(epToSub['lastsearch'], '%Y-%m-%d %H:%M:%S') > datetime.timedelta(hours=rules['new'][epToSub['searchcount']]))):
logger.log('Downloading subtitles for episode %dx%d of show %s' % (epToSub['season'], epToSub['episode'], epToSub['show_name']), logger.DEBUG)
showObj = helpers.findCertainShow(sickbeard.showList, int(epToSub['showid']))
if not showObj:
logger.log(u'Show not found', logger.DEBUG)
return
epObj = showObj.getEpisode(int(epToSub["season"]), int(epToSub["episode"]))
if isinstance(epObj, str):
logger.log(u'Episode not found', logger.DEBUG)
return
previous_subtitles = epObj.subtitles
try:
subtitles = epObj.downloadSubtitles()
except:

60
sickbeard/watchedstate.py Normal file
View file

@ -0,0 +1,60 @@
#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
import threading
import sickbeard
from sickbeard import watchedstate_queue
class WatchedStateUpdater(object):
def __init__(self, name, queue_item):
self.amActive = False
self.lock = threading.Lock()
self.name = name
self.queue_item = queue_item
@property
def prevent_run(self):
return sickbeard.watchedStateQueueScheduler.action.is_in_queue(self.queue_item)
def run(self):
if self.is_enabled():
self.amActive = True
new_item = self.queue_item()
sickbeard.watchedStateQueueScheduler.action.add_item(new_item)
self.amActive = False
class EmbyWatchedStateUpdater(WatchedStateUpdater):
def __init__(self):
super(EmbyWatchedStateUpdater, self).__init__('Emby', watchedstate_queue.EmbyWatchedStateQueueItem)
@staticmethod
def is_enabled():
return sickbeard.USE_EMBY and sickbeard.EMBY_WATCHEDSTATE_SCHEDULED
class PlexWatchedStateUpdater(WatchedStateUpdater):
def __init__(self):
super(PlexWatchedStateUpdater, self).__init__('Plex', watchedstate_queue.PlexWatchedStateQueueItem)
@staticmethod
def is_enabled():
return sickbeard.USE_PLEX and sickbeard.PLEX_WATCHEDSTATE_SCHEDULED

View file

@ -0,0 +1,83 @@
#
# This file is part of SickGear.
#
# SickGear is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickGear is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickGear. If not, see <http://www.gnu.org/licenses/>.
from __future__ import with_statement
from sickbeard import generic_queue, logger
from sickbeard.webserve import History
EMBYWATCHEDSTATE = 10
PLEXWATCHEDSTATE = 20
class WatchedStateQueue(generic_queue.GenericQueue):
def __init__(self):
super(WatchedStateQueue, self).__init__()
# self.queue_name = 'WATCHEDSTATEQUEUE'
self.queue_name = 'Q'
def is_in_queue(self, itemtype):
with self.lock:
for cur_item in self.queue + [self.currentItem]:
if isinstance(cur_item, itemtype):
return True
return False
# method for possible UI usage, can be removed if not used
def queue_length(self):
length = {'emby': 0, 'plex': 0}
with self.lock:
for cur_item in [self.currentItem] + self.queue:
if isinstance(cur_item, EmbyWatchedStateQueueItem):
length['emby'] += 1
elif isinstance(cur_item, PlexWatchedStateQueueItem):
length['plex'] += 1
return length
def add_item(self, item):
if isinstance(item, EmbyWatchedStateQueueItem) and not self.is_in_queue(EmbyWatchedStateQueueItem):
# emby watched state item
generic_queue.GenericQueue.add_item(self, item)
elif isinstance(item, PlexWatchedStateQueueItem) and not self.is_in_queue(PlexWatchedStateQueueItem):
# plex watched state item
generic_queue.GenericQueue.add_item(self, item)
else:
logger.log(u'Not adding item, it\'s already in the queue', logger.DEBUG)
class EmbyWatchedStateQueueItem(generic_queue.QueueItem):
def __init__(self):
super(EmbyWatchedStateQueueItem, self).__init__('Emby Watched', EMBYWATCHEDSTATE)
def run(self):
super(EmbyWatchedStateQueueItem, self).run()
try:
History.update_watched_state_emby()
finally:
self.finish()
class PlexWatchedStateQueueItem(generic_queue.QueueItem):
def __init__(self):
super(PlexWatchedStateQueueItem, self).__init__('Plex Watched', PLEXWATCHEDSTATE)
def run(self):
super(PlexWatchedStateQueueItem, self).run()
try:
History.update_watched_state_plex()
finally:
self.finish()

View file

@ -105,13 +105,26 @@ class Api(webserve.BaseHandler):
self.set_header('X-Application', 'SickGear')
self.set_header('X-API-Version', Api.version)
def prepare(self):
# Incorporate request JSON into arguments dictionary.
if self.request.body:
try:
json_data = {'payloadjson': json.loads(self.request.body)}
self.request.arguments.update(json_data)
except (StandardError, Exception):
raise ApiError('Unable to parse JSON.')
super(Api, self).prepare()
def post(self, route, *args, **kwargs):
return self.get(route, *args, **kwargs)
@gen.coroutine
def get(self, route, *args, **kwargs):
route = route.strip('/') or 'index'
kwargs = self.request.arguments
for arg, value in kwargs.items():
if len(value) == 1:
if not isinstance(value, dict) and len(value) == 1:
kwargs[arg] = value[0]
args = args[1:]
@ -475,6 +488,11 @@ class ApiCall(object):
value = li
else:
value = value.split("|")
elif type == "dict":
if isinstance(value, dict):
value = value
else:
error = True
elif type == "string":
pass
elif type == "ignore":
@ -2857,6 +2875,33 @@ class CMD_SickGearSetRequrieWords(ApiCall):
return _responds(RESULT_SUCCESS, data=return_data, msg="%s set requried words" % return_type)
class CMD_SickGearUpdateWatchedState(ApiCall):
_help = {"desc": "Update db with details of media file that is watched or unwatched",
"requiredParameters": {
"payloadjson": {
"desc": "Payload is a dict of dicts transmitted as JSON via POST request"},
}}
def __init__(self, handler, args, kwargs):
# required
self.payloadjson, args = self.check_params(args, kwargs, "payloadjson", None, True, "dict", [])
# optional
# super, missing, help
ApiCall.__init__(self, handler, args, kwargs)
def run(self):
""" Update db with details of media file that is watched or unwatched """
payload = self.payloadjson.copy()
from webserve import MainHandler
MainHandler.update_watched_state(payload, as_json=False)
if not payload:
return _responds(RESULT_FAILURE, msg='Request made to SickGear with invalid payload')
return _responds(RESULT_SUCCESS, payload)
class CMD_SickGearShow(ApiCall):
_help = {"desc": "display information for a given show",
"requiredParameters": {"indexer": {"desc": "indexer of a show"},
@ -4305,12 +4350,12 @@ class CMD_SickGearShowsStats(ApiCall):
return _responds(RESULT_SUCCESS, stats)
# WARNING: never define a cmd call string that contains a "_" (underscore)
class CMD_ShowsStats(CMD_SickGearShowsStats):
_help = {"desc": "display the global thetvdb.com shows and episode stats",
"SickGearCommand": "sg.shows.stats",
}
# this is reserved for cmd indexes used while cmd chaining
def __init__(self, handler, args, kwargs):
# required
# optional
@ -4318,8 +4363,8 @@ class CMD_ShowsStats(CMD_SickGearShowsStats):
self.sickbeard_call = True
CMD_SickGearShowsStats.__init__(self, handler, args, kwargs)
# WARNING: never define a cmd call string that contains a "_" (underscore)
# WARNING: never define a param name that contains a "." (dot)
# this is reserved for cmd namspaces used while cmd chaining
# this is reserved for cmd indexes used while cmd chaining
# WARNING: never define a param name that contains a "." (dot)
@ -4389,6 +4434,7 @@ _functionMaper = {"help": CMD_Help,
"sg.setignorewords": CMD_SickGearSetIgnoreWords,
"sg.listrequiredwords": CMD_SickGearListRequireWords,
"sg.setrequiredwords": CMD_SickGearSetRequrieWords,
"sg.updatewatchedstate": CMD_SickGearUpdateWatchedState,
"show": CMD_Show,
"sg.show": CMD_SickGearShow,
"show.addexisting": CMD_ShowAddExisting,

File diff suppressed because it is too large Load diff

View file

@ -93,6 +93,10 @@ class WebServer(threading.Thread):
# javascript
(r'%s/js/(.*)' % self.options['web_root'], webserve.BaseStaticFileHandler,
{'path': os.path.join(self.options['data_root'], 'js')}),
(r'%s/kodi/(.*)' % self.options['web_root'], webserve.RepoHandler,
{'path': os.path.join(sickbeard.CACHE_DIR, 'clients', 'kodi'),
'default_filename': 'index.html'}),
])
# Main Handler