Browse Source

Change free up some screen real estate on manage/Bulk Change.

Change rotate column headers on manage/Bulk Change for supported browsers to reduce screen estate waste.
Add column to manage/Bulk Change to display the show folder location size.
Add media stats to manage/Bulk Change and view-show when hovering over folder size.
Add to manage/Bulk Change an icon where show location no longer exists and group the icon/non icon shows.
Add a hover tip to the edit column on manage/Bulk Change to remind about using multi-select.
Change add tooltips on manage/Bulk Change checkbox actions to display what each are used for.
Add to manage/Bulk Change confirm dialog before removing or deleting a show.
Change manage/Bulk Change add sort by size options to table, (Total, Largest, Smallest, Average).
Change manage/Bulk Change add busy spinner for processing when changing size sort type.
Change manage/Bulk Change table make header stick when page is scrolled.
Change manage/Bulk Change table make footer stick when page is scrolled.
Change manage/Bulk Change add filter to table, showname, quality, and status.
Change number of shows listed after a filter is displayed at the bottom of Bulk Change.
Change edit and submit buttons are disabled when there is no selection on Bulk Change.
Change edit and submit buttons display number of selected items on Bulk Change.
Change tidy up html markup and JavaScript for manage/Bulk Change.
Change refactor to simplify bulk_change logic.
tags/release_0.25.1
JackDandy 4 years ago
parent
commit
3f69a7fddc
  1. 18
      CHANGES.md
  2. 28
      gui/slick/css/dark.css
  3. 27
      gui/slick/css/light.css
  4. 4
      gui/slick/css/style.css
  5. 2
      gui/slick/interfaces/default/displayShow.tmpl
  6. 237
      gui/slick/interfaces/default/manage.tmpl
  7. 263
      gui/slick/js/bulkChange.js
  8. 50
      gui/slick/js/inc_top.js
  9. 22
      sickbeard/helpers.py
  10. 6
      sickbeard/tv.py
  11. 206
      sickbeard/webserve.py

18
CHANGES.md

@ -1,5 +1,23 @@
### 0.24.0 (2021-xx-xx xx:xx:00 UTC) ### 0.24.0 (2021-xx-xx xx:xx:00 UTC)
* Change free up some screen real estate on manage/Bulk Change
* Change rotate column headers on manage/Bulk Change for supported browsers to reduce screen estate waste
* Add column to manage/Bulk Change to display the show folder location size
* Add media stats to manage/Bulk Change and view-show when hovering over folder size
* Add to manage/Bulk Change an icon where show location no longer exists and group the icon/non icon shows
* Add a hover tip to the edit column on manage/Bulk Change to remind about using multi-select
* Change add tooltips on manage/Bulk Change checkbox actions to display what each are used for
* Add to manage/Bulk Change confirm dialog before removing or deleting a show
* Change manage/Bulk Change add sort by size options to table, (Total, Largest, Smallest, Average)
* Change manage/Bulk Change add busy spinner for processing when changing size sort type
* Change manage/Bulk Change table make header stick when page is scrolled
* Change manage/Bulk Change table make footer stick when page is scrolled
* Change manage/Bulk Change add filter to table, showname, quality, and status
* Change number of shows listed after a filter is displayed at the bottom of Bulk Change
* Change edit and submit buttons are disabled when there is no selection on Bulk Change
* Change edit and submit buttons display number of selected items on Bulk Change
* Change tidy up html markup and JavaScript for manage/Bulk Change
* Change refactor to simplify bulk_change logic
* Add to config/General, "Package updates" and list packages, check packages by default on Windows, others must enable * Add to config/General, "Package updates" and list packages, check packages by default on Windows, others must enable
* Change simplify section config/General/Updates * Change simplify section config/General/Updates
* Add check for package updates to menu item action "Check for Updates" * Add check for package updates to menu item action "Check for Updates"

28
gui/slick/css/dark.css

@ -1545,6 +1545,10 @@ thead.tablesorter-stickyHeader{
background-color:#2e2e2e background-color:#2e2e2e
} }
.sort-size-type.tablesorter-headerSorted{
background-color:#2c6590
}
.tablesorter tr.tablesorter-filter-row, .tablesorter tr.tablesorter-filter-row,
.tablesorter tr.tablesorter-filter-row td{ .tablesorter tr.tablesorter-filter-row td{
text-align:center; text-align:center;
@ -1552,11 +1556,22 @@ thead.tablesorter-stickyHeader{
border-bottom:1px solid #111 border-bottom:1px solid #111
} }
.tablesorter-filter-row input[type="search"]{
box-shadow:inset rgba(255, 255, 255, 0.2) 0 1px 0
}
#tfoot{
text-shadow:-1px -1px 0 rgba(0, 0, 0, 0.3)
}
#tfoot,
.tablesorter tfoot tr{ .tablesorter tfoot tr{
color:#ddd; color:#ddd;
text-align:center; text-align:center;
text-shadow:-1px -1px 0 rgba(0, 0, 0, 0.3); background-color:#333
background-color:#333; }
.tablesorter tfoot tr{
border-collapse:collapse border-collapse:collapse
} }
@ -1564,6 +1579,15 @@ thead.tablesorter-stickyHeader{
color:#ddd color:#ddd
} }
#bulk-change-table .tablesorter-filter-row,
#bulk-change-table .tablesorter-filter-row td,
#bulk-change-table-sticky .tablesorter-filter-row,
#bulk-change-table-sticky .tablesorter-filter-row td,
#tfoot{
background:#282828;
border-bottom:0
}
#showListTable tbody{ #showListTable tbody{
color:#000 color:#000
} }

27
gui/slick/css/light.css

@ -1470,6 +1470,10 @@ thead.tablesorter-stickyHeader{
background-color:#dfdacf background-color:#dfdacf
} }
.sort-size-type.tablesorter-headerSorted{
background-color:#4a4a4a
}
.tablesorter tr.tablesorter-filter-row, .tablesorter tr.tablesorter-filter-row,
.tablesorter tr.tablesorter-filter-row td{ .tablesorter tr.tablesorter-filter-row td{
text-align:center; text-align:center;
@ -1477,11 +1481,23 @@ thead.tablesorter-stickyHeader{
border-bottom:1px solid #ddd border-bottom:1px solid #ddd
} }
.tablesorter-filter-row input[type="search"]{
box-shadow:inset rgba(0, 0, 0, 0.2) 0 0 0 1px
}
#tfoot{
text-shadow:-1px -1px 0 rgba(255, 255, 255, 0.6)
}
#tfoot,
.tablesorter tfoot tr{ .tablesorter tfoot tr{
color:#ddd; color:#ddd;
text-align:center; text-align:center;
background-color:#333
}
.tablesorter tfoot tr{
text-shadow:-1px -1px 0 rgba(0, 0, 0, 0.3); text-shadow:-1px -1px 0 rgba(0, 0, 0, 0.3);
background-color:#333;
border-collapse:collapse border-collapse:collapse
} }
@ -1489,6 +1505,15 @@ thead.tablesorter-stickyHeader{
color:#fff color:#fff
} }
#bulk-change-table .tablesorter-filter-row,
#bulk-change-table .tablesorter-filter-row td,
#bulk-change-table-sticky .tablesorter-filter-row,
#bulk-change-table-sticky .tablesorter-filter-row td,
#tfoot{
background:#d7d7d7;
border-bottom:0
}
/* ======================================================================= /* =======================================================================
jquery.confirm.css jquery.confirm.css
========================================================================== */ ========================================================================== */

4
gui/slick/css/style.css

@ -4806,7 +4806,8 @@ tablesorter.css
} }
.tablesorter th, .tablesorter th,
.tablesorter td{ .tablesorter td,
#tfoot{
padding:4px; padding:4px;
/*border-top:1px solid;*/ /*border-top:1px solid;*/
/*border-left:1px solid;*/ /*border-left:1px solid;*/
@ -4836,6 +4837,7 @@ tablesorter.css
vertical-align:middle vertical-align:middle
} }
.tablesorter thead .sort-size-type.tablesorter-headerSorted,
#display-show .tablesorter .tablesorter-header, #display-show .tablesorter .tablesorter-header,
#display-show .tablesorter thead .tablesorter-headerDesc, #display-show .tablesorter thead .tablesorter-headerDesc,
#display-show .tablesorter thead .tablesorter-headerAsc{background-image:none} #display-show .tablesorter thead .tablesorter-headerAsc{background-image:none}

2
gui/slick/interfaces/default/displayShow.tmpl

@ -333,7 +333,7 @@
<div id="details-bottom"> <div id="details-bottom">
<span class="label addQTip" title="Info language, $show_obj.lang"><img src="$sbRoot/images/flags/${show_obj.lang}.png" width="16" height="11" alt="" style="margin-top:-1px" /></span> <span class="label addQTip" title="Info language, $show_obj.lang"><img src="$sbRoot/images/flags/${show_obj.lang}.png" width="16" height="11" alt="" style="margin-top:-1px" /></span>
<span class="label addQTip" title="Location#echo (' no longer exists" style="background-color:#8f1515"', '"')[$showLoc[1]]#>$showLoc[0]</span> <span class="label addQTip" title="Location#echo (' no longer exists" style="background-color:#8f1515"', '"')[$showLoc[1]]#>$showLoc[0]</span>
<span class="label addQTip" title="Size">$human($get_size($showLoc[0]))</span> <span id="data-size" class="label" style="cursor:help">$human($get_size($showLoc[0]))</span>
#set $filecount = sum([$c for $k, $c in $ep_counts['videos'].items()]) #set $filecount = sum([$c for $k, $c in $ep_counts['videos'].items()])
#set $to_prune = $show_obj.prune - $filecount #set $to_prune = $show_obj.prune - $filecount
#set $keep_or_prune = ('', ' (%s)' % ('%s to prune' % abs($to_prune), 'keep %s' % $show_obj.prune)[0 <= $to_prune])[bool($show_obj.prune)] #set $keep_or_prune = ('', ' (%s)' % ('%s to prune' % abs($to_prune), 'keep %s' % $show_obj.prune)[0 <= $to_prune])[bool($show_obj.prune)]

237
gui/slick/interfaces/default/manage.tmpl

@ -1,25 +1,23 @@
#import sickbeard #import sickbeard
#from sickbeard.common import * #from sickbeard.common import *
#from sickbeard.helpers import get_size, human
## ##
#set global $title = 'Bulk Change' #set global $title = 'Bulk Change'
#set global $header = 'Bulk Change' #set global $header = 'Bulk Change'
#set global $sbPath = '../..' #set global $sbPath = '../..'
#set global $topmenu = 'manage' #set global $topmenu = 'manage'
<% def sg_var(varname, default=False): return getattr(sickbeard, varname, default) %>#slurp#
<% def sg_str(varname, default=''): return getattr(sickbeard, varname, default) %>#slurp#
## ##
#import os.path #import os.path
#include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl') #include $os.path.join($sickbeard.PROG_DIR, 'gui/slick/interfaces/default/inc_top.tmpl')
## ##
#set $has_any_sports = False
#set $has_any_anime = False
#set $has_any_flat_folders = False
#set $show_list = sorted($sickbeard.showList, key=lambda _x: _x.name)
#for $cur_show_obj in $show_list
#set $has_any_sports |= bool($cur_show_obj.sports)
#set $has_any_anime |= bool($cur_show_obj.anime)
#set $has_any_flat_folders |= $bool(cur_show_obj.flatten_folders)
#end for
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
<!-- <!--
## an arbitrary high number greater than a media file size
#set $high = 1000000000000
\$.SickGear.high = $high;
\$.tablesorter.addParser({ \$.tablesorter.addParser({
id: 'showNames', id: 'showNames',
is: function(s) { return !1; }, is: function(s) { return !1; },
@ -38,40 +36,56 @@
type: 'numeric' type: 'numeric'
}); });
## ##
#set $columns_total = 15 - ((1, 0)[$has_any_sports] + (1, 0)[$has_any_anime] + (1, 0)[$has_any_flat_folders] + (1, 0)[$sickbeard.USE_SUBTITLES]) #set $enable_tvinfo = False
#set $column_headers = [('false', False), ("'showNames'", False), ("'quality'", False), #set $column_headers = [
((None, "'sports'")[$has_any_sports], True), ('!1', '!1', False),
("'scene'", True), ((None, "'anime'")[$has_any_anime], True), ("'showNames'", '!0', False),
((None, "'flatfold'")[$has_any_flat_folders], True), ("'paused'", True), ("'size'", '!1', False), ('!1', '!1', False),
("'status'", False), ('false', False), ('false', False), ('false', False), ((None, "'tvinfo'")[$enable_tvinfo], '!1', False),
((None, 'false')[$sickbeard.USE_SUBTITLES], False), ('false', False), ('false', False)] ("'quality'", '!0', False),
((None, "'sports'")[$has_any_sports], '!1', True),
("'scene'", '!1', True),
((None, "'anime'")[$has_any_anime], '!1', True),
((None, "'flatfold'")[$has_any_flat_folders], '!1', True),
("'paused'", '!1', True), ("'status'", '!0', False),
('!1', '!1', False), ('!1', '!1', False), ('!1', '!1', False), ((None, '!1')[$sickbeard.USE_SUBTITLES], '!1', False), ('!1', '!1', False), ('!1', '!1', False)
]
#set $columns_total = $len($column_headers) - ((1, 0)[$has_any_sports] + (1, 0)[$has_any_anime] + (1, 0)[$has_any_flat_folders] + (1, 0)[$sickbeard.USE_SUBTITLES])
#set $headers = [] #set $headers = []
#set $text_extract = [] #set $text_extract = []
#set $column = -1 #set $column = -1
#for $k, ($c, $img_extract) in enumerate($column_headers) #for $sort, $filter, $img_extract in $column_headers
#if None is $c #if None is $sort
#continue #continue
#end if #end if
#set $column += 1 #set $column += 1
#set void = $headers.append('\t\t\t%s: { sorter: %s }' % ($column, $c)) #set void = $headers.append('\t\t\t%s: {sorter:%s, filter:%s}' % ($column, $sort, $filter))
#if $img_extract #if $img_extract
#set void = $text_extract.append('\t\t\t%s%s' % ($column, ": function(node) {return $(node).find('img').attr('alt')}")) #set void = $text_extract.append('\t\t\t%s%s' % ($column, ": function(node) {return $(node).find('img').attr('alt')}"))
#end if #end if
#end for #end for
\$(document).ready(function() \$(function()
{ {
\$('#bulkChangeTable:has(tbody tr)').tablesorter({ \$('#bulk-change-table:has(tbody tr)').tablesorter({
widgets: ['zebra'], widgets: ['zebra','stickyHeaders', 'filter'],
sortList: [[1,0]], sortList: [[1,0]],
headers: { headers: {
#echo ',\n'.join($headers)# #echo ',\n'.join($headers)#
}, },
textExtraction: { textExtraction: {
2: function(node) {return \$(node).find('span').text().toLowerCase()}, #raw
1: function(node) {return ((!!$(node).find('i.img-warning-16').length ? '1_' : '0_') + $(node).text().toLowerCase())},
2: function(node) {return parseInt($(node).closest('[data-tvid_prodid]').attr('data-size') || -100, 10)},
#end raw
#if $enable_tvinfo
4: function(node) {return \$(node).find('i').attr('data-tvid')},
#end if
#echo ('5','4')[not $enable_tvinfo]#: function(node) {return \$(node).find('span').text().toLowerCase()},
#echo ',\n'.join($text_extract)# #echo ',\n'.join($text_extract)#
} }
}); });
}); });
//--> //-->
</script> </script>
@ -81,129 +95,174 @@
#else #else
<h1 class="title">$title</h1> <h1 class="title">$title</h1>
#end if #end if
<style>
.tvShow{text-align:left}
.tvShow i{margin-right:6px; margin-bottom:1px}
.rotate-body span{float:left}
@supports (transform:translateX(16px) rotate(-45deg)) and (transform-origin:bottom left){
.tablesorter.rotate th.tablesorter-header.sort-icon-left{background-position:center left; padding: 4px 4px 4px 18px}
.tablesorter.rotate th.tablesorter-header.sort-icon-bottom{background-position:10px 54px /*28px 45px*/}
.tablesorter.rotate th.tablesorter-header.sort-icon-status{background-position:30px 54px}
.tablesorter.rotate th.tablesorter-header.sort-size{background-position:26px 54px; padding:4px 14px 4px 14px}
.tablesorter.rotate th.tablesorter-header.sort-icon-quality{background-position:20px 54px}
.rotate-holder{width:20px; margin-bottom:-16px}
.rotate-holder2{width:20px; margin:22px 0 -22px}
.rotate-body{width:25px; transform-origin:bottom left; transform:translateX(16px) rotate(-45deg)}
.rotate-body2{width:25px; transform-origin:bottom left; transform:translateX(20px) rotate(-45deg)}
.rotate-body span{float:none}
.rotate th.col-legend, .rotate td.col-legend{width:40px}
.rotate input[type="checkbox"].right-2px{margin:0 2px 0 0}
.rotate .img-warning-16{cursor:help}
}
#bulk-change-table thead,
#bulk-change-table-sticky thead,
#tfoot{position:sticky}
#bulk-change-table thead,
#bulk-change-table-sticky thead{top:50px}
#bulk-change-table-sticky,.tablesorter thead,.tablesorter thead tr,.tablesorter thead tr th, .tablesorter thead tr td{border-spacing:0}
#tfoot{bottom:0; min-height:34px; line-height:26px}
#tfoot input[disabled]{cursor:not-allowed}
.tablesorter-sticky-wrapper{margin-top:50px}
.tablesorter-filter-row input[disabled]{display:none}
.tablesorter-filter-row input[type="search"]{background:rgba(255,255,255,0.2); padding:0 3px; border:0; border-radius:4px; color:inherit}
.tablesorter .tablesorter-header.sort-size-type{padding:0; width:16px; cursor:pointer}
th.sort-size-type{font-weight:normal}
.average{border-top:1px solid white; width:9px; margin:0 auto}
.average i{display:block; line-height:10px; margin-right:1px}
#set $theme_suffix = ('', '-dark')['dark' == $sg_str('THEME_NAME', 'dark')]
.tip,.tip-average{margin:0 2px}
.tip-average{border-top:1px solid #echo ('#999', '#666')[not $theme_suffix]#}
.tip-average,.tip-average i{display:inline-block; line-height:8px}
.bfr{position:absolute;left:-999px;top:-999px}.bfr img,.spinner{display:inline-block;width:16px;height:16px}.spinner{background:url(${sbRoot}/images/loading16${theme_suffix}.gif) no-repeat 0 0}
</style>
<div class="bfr"><img src="$sbRoot/images/loading16${theme_suffix}.gif"></div>
<form name="bulkChangeForm" method="post" action="bulk_change"> <form name="bulkChangeForm" method="post" action="bulk_change">
$xsrf_form_html $xsrf_form_html
<table id="bulkChangeTable" class="sickbeardTable tablesorter" cellspacing="1" border="0" cellpadding="0"> <table id="bulk-change-table" class="sickbeardTable tablesorter rotate" cellspacing="1" border="0" cellpadding="0">
<thead> <thead>
<tr> <tr style="height:70px"> <!-- must inline this css -->
<th class="col-checkbox">Edit<br /><input type="checkbox" class="bulkCheck" id="editCheck"></th> <th class="col-checkbox"><div>Edit</div><input type="checkbox" class="bulk-check" id="edit-check"></th>
<th class="nowrap narrow" style="text-align:left">Show Name</th> <th class="text-nowrap narrow sort-icon-left" style="text-align:left">Show Name</th>
<th class="col-legend narrow">Quality</th> <th class="sort-size col-legend narrow dir-none"><div class="rotate-holder"><div class="rotate-body">Size</div></div></th>
<th class="narrow sort-size-type" data-type="E" title="total"><div class="sort-size-type-body">&Sigma;</div><span class="sort-size-type-image" style="display:none; margin-top:8px"><i class="spinner"></i></span></th>
#if $enable_tvinfo
<th class="col-legend narrow sort-icon-bottom"><div class="rotate-holder"><div class="rotate-body">TV Info</div></div></th>
#end if
<th class="col-legend narrow sort-icon-quality"><div class="rotate-holder"><div class="rotate-body">Quality</div></div></th>
#if $has_any_sports #if $has_any_sports
<th class="col-legend narrow">Sports</th> <th class="col-legend narrow sort-icon-bottom"><div class="rotate-holder"><div class="rotate-body">Sports</div></div></th>
#end if #end if
<th class="col-legend narrow">Scene</th> <th class="col-legend narrow sort-icon-bottom"><div class="rotate-holder"><div class="rotate-body2">Scene</div></div></th>
#if $has_any_anime #if $has_any_anime
<th class="col-legend narrow">Anime</th> <th class="col-legend narrow sort-icon-bottom"><div class="rotate-holder"><div class="rotate-body2">Anime</div></div></th>
#end if #end if
#if $has_any_flat_folders #if $has_any_flat_folders
<th class="col-legend narrow">Flat<br /> Folders</th> <th class="col-legend narrow sort-icon-bottom"><div class="rotate-holder"><div class="rotate-body2">Flat Folders</div></div></th>
#end if #end if
<th class="col-legend narrow">Paused</th> <th class="col-legend narrow sort-icon-bottom"><div class="rotate-holder"><div class="rotate-body2">Paused</div></div></th>
<th class="col-legend narrow">Status</th> <th class="col-legend narrow sort-icon-status"><div class="rotate-holder"><div class="rotate-body2">Status</div></div></th>
<th width="1%">Update<br /><input type="checkbox" class="bulkCheck" id="updateCheck"></th> <th width="1%"><div class="rotate-holder2"><div class="rotate-body"><label><input type="checkbox" class="bulk-check right-2px" id="update-check"><span>Update</span></label></div></div></th>
<th width="1%">Rescan<br /><input type="checkbox" class="bulkCheck" id="refreshCheck"></th> <th width="1%"><div class="rotate-holder2"><div class="rotate-body"><label><input type="checkbox" class="bulk-check right-2px" id="refresh-check"><span>Rescan</span></label></div></div></th>
<th width="1%">Rename<br /><input type="checkbox" class="bulkCheck" id="renameCheck"></th> <th width="1%"><div class="rotate-holder2"><div class="rotate-body"><label><input type="checkbox" class="bulk-check right-2px" id="rename-check"><span>Rename</span></label></div></div></th>
#if $sickbeard.USE_SUBTITLES #if $sickbeard.USE_SUBTITLES
<th width="1%">Search<br />Subtitle<br /><input type="checkbox" class="bulkCheck" id="subtitleCheck"></th> <th width="1%"><div class="rotate-holder2"><div class="rotate-body"><label><input type="checkbox" class="bulk-check right-2px" id="subtitleCheck"><span>Subtitle</span></label></div></div></th>
#end if #end if
## <!-- <th>Force Metadata Regen <input type="checkbox" class="bulkCheck" id="metadataCheck"></th>//--> <th width="1%"><div class="rotate-holder2"><div class="rotate-body"><label><input type="checkbox" class="bulk-check right-2px" id="delete-check"><span>Delete</span></label></div></div></th>
<th width="1%">Delete<br /><input type="checkbox" class="bulkCheck" id="deleteCheck"></th> <th width="1%"><div class="rotate-holder2" style="margin-right:40px;"><div class="rotate-body"><label><input type="checkbox" class="bulk-check right-2px" id="remove-check"><span>Remove</span></label></div></div></th>
<th width="1%">Remove<br /><input type="checkbox" class="bulkCheck" id="removeCheck"></th>
</tr> </tr>
</thead> </thead>
<tfoot>
<tr>
<td rowspan="1" colspan="2" class="align-center alt"><input class="btn pull-left" type="button" value="Edit Selected" id="submitMassEdit"></td>
<td rowspan="1" colspan="#echo $columns_total-2#" class="align-right alt"><input class="btn pull-right" type="button" value="Submit" id="submitBulkChange"></td>
</tr>
</tfoot>
<tbody> <tbody>
#set $disabled = ' disabled="disabled"' #set $disabled = ' disabled="disabled"'
#set $disabled_inprogress_tip = ' title="%s action is currently in progress for this show"' #set $disabled_inprogress_tip = ' action is currently in progress for this show'
#set $disabled_subtitles_tip = ' title="Use edit to enable subtitle search for this show"' #set $disabled_subtitles_tip = ' title="Use edit to enable subtitle search for this show"'
#set $no = 'no16.png" title="No" alt="No' #set $no = 'no16.png" title="No" alt="No'
#set $yes = 'yes16.png" title="Yes" alt="Yes' #set $yes = 'yes16.png" title="Yes" alt="Yes'
#for $cur_show_obj in $show_list #set $max = $high + 100
#set $option_state = '<input type="checkbox" class="%sCheck" id="%s-{0:s}"%s>'.format($cur_show_obj.tvid_prodid) #for $cur_show_obj in $shows + $shows_no_loc
#set $show_loc = $cur_show_obj.path
#set $show_size = $max if not $show_loc else $get_size($show_loc)
#set $option_state = '<input type="checkbox"%s class="%s-check"%s>'
## ##
#set $curUpdate_disabled = $sickbeard.show_queue_scheduler.action.isBeingUpdated($cur_show_obj)\ #set $curUpdate_disabled = $sickbeard.show_queue_scheduler.action.isBeingUpdated($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInUpdateQueue($cur_show_obj) or $sickbeard.show_queue_scheduler.action.isInUpdateQueue($cur_show_obj)
#set $reason = $disabled_inprogress_tip % 'Update' #set $tip = ' title="Update%s"' % ('', $disabled_inprogress_tip)[$curUpdate_disabled]
#set $curUpdate = '%s>%s' % (('', $reason)[$curUpdate_disabled], #set $curUpdate = ($tip, $option_state % (('', $disabled)[$curUpdate_disabled], 'update', $tip))
$option_state % ('update', 'update', ('', $disabled + $reason)[$curUpdate_disabled]))
## ##
#set $curRefresh_disabled = $sickbeard.show_queue_scheduler.action.isBeingRefreshed($cur_show_obj)\ #set $curRefresh_disabled = $sickbeard.show_queue_scheduler.action.isBeingRefreshed($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInRefreshQueue($cur_show_obj) or $sickbeard.show_queue_scheduler.action.isInRefreshQueue($cur_show_obj)
#set $reason = $disabled_inprogress_tip % 'Rescan' #set $tip = ' title="Rescan%s"' % ('', $disabled_inprogress_tip)[$curRefresh_disabled]
#set $curRefresh = '%s>%s' % (('', $reason)[$curRefresh_disabled], #set $curRefresh = ($tip, $option_state % (('', $disabled)[$curRefresh_disabled], 'refresh', $tip))
$option_state % ('refresh', 'refresh', ('', $disabled + $reason)[$curRefresh_disabled]))
## ##
#set $curRename_disabled = $sickbeard.show_queue_scheduler.action.isBeingRenamed($cur_show_obj)\ #set $curRename_disabled = $sickbeard.show_queue_scheduler.action.isBeingRenamed($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInRenameQueue($cur_show_obj) or $sickbeard.show_queue_scheduler.action.isInRenameQueue($cur_show_obj)
#set $reason = $disabled_inprogress_tip % 'Rename' #set $tip = ' title="Rename%s"' % ('', $disabled_inprogress_tip)[$curRename_disabled]
#set $curRename = '%s>%s' % (('', $reason)[$curRename_disabled], #set $curRename = ($tip, $option_state % (('', $disabled)[$curRename_disabled], 'rename', $tip))
$option_state % ('rename', 'rename', ('', $disabled + $reason)[$curRename_disabled]))
## ##
#set $subtitles_disabled = not $cur_show_obj.subtitles\ #set $subtitles_disabled = not $cur_show_obj.subtitles\
or $sickbeard.show_queue_scheduler.action.isBeingSubtitled($cur_show_obj)\ or $sickbeard.show_queue_scheduler.action.isBeingSubtitled($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInSubtitleQueue($cur_show_obj) or $sickbeard.show_queue_scheduler.action.isInSubtitleQueue($cur_show_obj)
#set $reason = ($disabled_inprogress_tip % 'Search subtitle', $disabled_subtitles_tip)[not $cur_show_obj.subtitles] #set $tip = (' title="Search subtitle"', (' title="Search subtitle%s"' % $disabled_inprogress_tip,
#set $curSubtitle = '%s>%s' % (('', $reason)[$subtitles_disabled], $disabled_subtitles_tip)[not $cur_show_obj.subtitles])[$subtitles_disabled]
$option_state % ('subtitle', 'subtitle', ('', $disabled + $reason)[$subtitles_disabled])) #set $curSubtitle = ($tip, $option_state % (('', $disabled)[$subtitles_disabled], 'subtitle', $tip))
## ##
#set $curDelete_disabled = $sickbeard.show_queue_scheduler.action.isBeingRenamed($cur_show_obj)\ #set $curDelete_disabled = $sickbeard.show_queue_scheduler.action.isBeingRenamed($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInRenameQueue($cur_show_obj)\ or $sickbeard.show_queue_scheduler.action.isInRenameQueue($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInRefreshQueue($cur_show_obj) or $sickbeard.show_queue_scheduler.action.isInRefreshQueue($cur_show_obj)
#set $reason = $disabled_inprogress_tip % 'Rename or rescan' #set $tip = ' title="Delete%s"' % ('', $disabled_inprogress_tip)[$curDelete_disabled]
#set $curDelete = '%s>%s' % (('', $reason)[$curDelete_disabled], #set $curDelete = ($tip, $option_state % (('', $disabled)[$curDelete_disabled], 'delete', $tip))
$option_state % ('delete', 'delete', ('', $disabled + $reason)[$curDelete_disabled]))
## ##
#set $curRemove_disabled = $sickbeard.show_queue_scheduler.action.isBeingRenamed($cur_show_obj)\ #set $curRemove_disabled = $sickbeard.show_queue_scheduler.action.isBeingRenamed($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInRenameQueue($cur_show_obj)\ or $sickbeard.show_queue_scheduler.action.isInRenameQueue($cur_show_obj)\
or $sickbeard.show_queue_scheduler.action.isInRefreshQueue($cur_show_obj) or $sickbeard.show_queue_scheduler.action.isInRefreshQueue($cur_show_obj)
##set $reason = $disabled_inprogress_tip % 'Rename or rescan' #set $tip = ' title="Remove%s"' % ('', $disabled_inprogress_tip)[$curRemove_disabled]
#set $curRemove = '%s>%s' % (('', $reason)[$curRemove_disabled], #set $curRemove = ($tip, $option_state % (('', $disabled)[$curRemove_disabled], 'remove', $tip))
$option_state % ('remove', 'remove', ('', $disabled + $reason)[$curRemove_disabled])) <tr data-tvid_prodid="$cur_show_obj.tvid_prodid" data-size="$show_size">
<tr> <td><input type="checkbox" class="edit-check"></td>
<td align="center"><input type="checkbox" class="editCheck" id="edit-$cur_show_obj.tvid_prodid"></td>
#set $display_name = (re.sub(r'^((?:A(?!\s+to)n?)|The)\s(\w)', r'<span class="article">\1</span> \2', $cur_show_obj.name), $cur_show_obj.name)[$sickbeard.SORT_ARTICLE] #set $display_name = (re.sub(r'^((?:A(?!\s+to)n?)|The)\s(\w)', r'<span class="article">\1</span> \2', $cur_show_obj.name), $cur_show_obj.name)[$sickbeard.SORT_ARTICLE]
<td class="tvShow"><a href="$sbRoot/home/view-show?tvid_prodid=${cur_show_obj.tvid_prodid}">$display_name</a></td> <td class="tvShow">#if not $show_loc#<i class="img-warning-16" title="Location no longer exists"></i>#end if#<a href="$sbRoot/home/view-show?tvid_prodid=${cur_show_obj.tvid_prodid}">$display_name</a></td>
<td colspan=2>#if 0 <= $show_size < $max#<span class="text-nowrap ui-size">$human($show_size)</span>#end if#</td>
#if $enable_tvinfo
<td><i class="icon16 $sickbeard.TVInfoAPI($cur_show_obj.tvid).config['slug']" data-tvid="$cur_show_obj.tvid"></i></td>
#end if
#if $cur_show_obj.quality in $qualityPresets #if $cur_show_obj.quality in $qualityPresets
<td align="center"><span class="quality $qualityPresetStrings[$cur_show_obj.quality]">$qualityPresetStrings[$cur_show_obj.quality]</span></td> <td><span class="quality $qualityPresetStrings[$cur_show_obj.quality]">$qualityPresetStrings[$cur_show_obj.quality]</span></td>
#else #else
<td align="center"><span class="quality Custom">Custom</span></td> <td><span class="quality Custom">Custom</span></td>
#end if #end if
#if $has_any_sports #if $has_any_sports
<td align="center"><img src="$sbRoot/images/#if 1 == int($cur_show_obj.is_sports) then $yes else $no#" width="16" height="16" /></td> <td><img src="$sbRoot/images/#if 1 == int($cur_show_obj.is_sports) then $yes else $no#" width="16" height="16"></td>
#end if #end if
<td align="center"><img src="$sbRoot/images/#if 1 == int($cur_show_obj.is_scene) then $yes else $no#" width="16" height="16" /></td> <td><img src="$sbRoot/images/#if 1 == int($cur_show_obj.is_scene) then $yes else $no#" width="16" height="16"></td>
#if $has_any_anime #if $has_any_anime
<td align="center"><img src="$sbRoot/images/#if 1 == int($cur_show_obj.is_anime) then $yes else $no#" width="16" height="16" /></td> <td><img src="$sbRoot/images/#if 1 == int($cur_show_obj.is_anime) then $yes else $no#" width="16" height="16"></td>
#end if #end if
#if $has_any_flat_folders #if $has_any_flat_folders
<td align="center"><img src="$sbRoot/images/#if 1 == int($cur_show_obj.flatten_folders) then $yes else $no#" width="16" height="16" /></td> <td><img src="$sbRoot/images/#if 1 == int($cur_show_obj.flatten_folders) then $yes else $no#" width="16" height="16"></td>
#end if #end if
<td align="center"><img src="$sbRoot/images/#if 1 == int($cur_show_obj.paused) then $yes else $no#" width="16" height="16" /></td> <td><img src="$sbRoot/images/#if 1 == int($cur_show_obj.paused) then $yes else $no#" width="16" height="16"></td>
<td align="center">$cur_show_obj.status</td> <td>$cur_show_obj.status</td>
<td align="center"$curUpdate</td> <td$curUpdate[0]>$curUpdate[1]</td>
<td align="center"$curRefresh</td> <td$curRefresh[0]>$curRefresh[1]</td>
<td align="center"$curRename</td> <td$curRename[0]>$curRename[1]</td>
#if $sickbeard.USE_SUBTITLES #if $sickbeard.USE_SUBTITLES
<td align="center"$curSubtitle</td> <td$curSubtitle[0]>$curSubtitle[1]</td>
#end if #end if
<td align="center"$curDelete</td> <td$curDelete[0]>$curDelete[1]</td>
<td align="center"$curRemove</td> <td$curRemove[0]><span style="margin-right: 40px">$curRemove[1]</span></td>
</tr> </tr>
#end for #end for
</tbody> </tbody>
</table> </table>
<div id="tfoot">
<span style="width:20%" class="pull-left"><input class="btn pull-left" type="button" value="Edit Selected" id="bulk-change-edit"></span>
<span class="stats grey-text"></span>
<span style="width:20%" class="pull-right"><input class="btn pull-right" type="button" value="Submit" id="bulk-change-submit"></span>
</div>
</form> </form>
#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')

263
gui/slick/js/bulkChange.js

@ -1,112 +1,223 @@
/** @namespace $.SickGear.Root */ /** @namespace $.SickGear.Root */
$(document).ready(function() { $(function(){
$('#submitMassEdit').click(function() { $('#bulk-change-edit').click(function(){
var editArr = []; var toDo = [];
$('.editCheck').each(function() { $('.edit-check:checked').each(function(){
if (true == this.checked) { toDo.push($(this).closest('tr').attr('data-tvid_prodid'));
editArr.push($(this).attr('id').split('-')[1])
}
}); });
if (0 == editArr.length) if (0 === toDo.length)
return !1; return !1;
window.location.href = $.SickGear.Root + '/manage/mass-edit?to_edit=' + editArr.join('|'); window.location.href = $.SickGear.Root + '/manage/mass-edit?to_edit=' + toDo.join('|');
}); });
$('#bulk-change-submit').click(function(){
$('#submitBulkChange').click(function() { var toChange = !1, toDo = {update: [], refresh: [], rename: [], subtitle: [], delete: [], remove: []};
var updateArr = [], refreshArr = [], renameArr = [], subtitleArr = [],
deleteArr = [], removeArr = [], metadataArr = [];
$('.updateCheck').each(function() {
if (true == this.checked) {
updateArr.push($(this).attr('id').split('-')[1])
}
});
$('.refreshCheck').each(function() {
if (true == this.checked) {
refreshArr.push($(this).attr('id').split('-')[1])
}
});
$('.renameCheck').each(function() {
if (true == this.checked) {
renameArr.push($(this).attr('id').split('-')[1])
}
});
$('.subtitleCheck').each(function() {
if (true == this.checked) {
subtitleArr.push($(this).attr('id').split('-')[1])
}
});
$('.deleteCheck').each(function() {
if (true == this.checked) {
deleteArr.push($(this).attr('id').split('-')[1])
}
});
$('.removeCheck').each(function() { $.each(Object.keys(toDo), function(i, k){
if (true == this.checked) { $('.' + k + '-check:checked').each(function(){
removeArr.push($(this).attr('id').split('-')[1]) toDo[k].push($(this).closest('tr').attr('data-tvid_prodid'));
} toChange = !0;
});
}); });
/* if (!toChange)
$('.metadataCheck').each(function() {
if (true == this.checked) {
metadataArr.push($(this).attr('id').split('-')[1])
}
});
*/
if (0 == updateArr.length + refreshArr.length + renameArr.length + subtitleArr.length + deleteArr.length + removeArr.length + metadataArr.length)
return !1; return !1;
window.location.href = $.SickGear.Root + '/manage/bulk-change?to_update=' + updateArr.join('|') + '&to_refresh=' + refreshArr.join('|') + '&to_rename=' + renameArr.join('|') + '&to_delete=' + deleteArr.join('|') + '&to_remove=' + removeArr.join('|') + '&to_metadata=' + metadataArr.join('|') + '&to_subtitle=' + subtitleArr.join('|'); var confirmArr = [],
command = function(){
window.location.href = $.SickGear.Root + '/manage/bulk-change?'
+ 'to_update=' + toDo.update.join('|')
+ '&to_refresh=' + toDo.refresh.join('|')
+ '&to_rename=' + toDo.rename.join('|')
+ '&to_subtitle=' + toDo.subtitle.join('|')
+ '&to_delete=' + toDo.delete.join('|')
+ '&to_remove=' + toDo.remove.join('|');
return !0;
};
if(toDo.delete.length){
confirmArr.push('delete ' + toDo.delete.length);
}
if(toDo.remove.length){
confirmArr.push('remove ' + toDo.remove.length);
}
if(!confirmArr.length){
command();
} else {
$.confirm({
'title': 'Are you sure ?',
'message': confirmArr.join(' and ') + ' show(s)',
'buttons': {
'Yes': {
'class': 'green',
'action': command
},
'No': {
'class': 'red',
'action': function(){
} // No op. This action property can be omitted.
}
}
});
}
}); });
$('.bulkCheck').click(function() { var updateButtons = function(){
var editBtn$ = $('#bulk-change-edit'), submitBtn$ = $('#bulk-change-submit'), numEdits = $('.edit-check:checked').length,
var bulkCheck = this, whichBulkCheck = $(bulkCheck).attr('id'); numToDo = $('.update-check:checked, .refresh-check:checked, .rename-check:checked, .delete-check:checked, .remove-check:checked').length;
if(!!numEdits){
$('.' + whichBulkCheck).each(function() { editBtn$.attr('disabled', !1);
editBtn$.val('Edit ' + numEdits + ' selected');
} else {
editBtn$.attr('disabled', !0);
editBtn$.val('Select items');
}
if(!!numToDo){
submitBtn$.attr('disabled', !1);
submitBtn$.val('Submit ' + numToDo + ' action(s)');
} else {
submitBtn$.attr('disabled', !0);
submitBtn$.val('Select actions');
}
}
updateButtons();
$('.bulk-check').click(function(){
$('.' + $(this).attr('id')).each(function(){
if (!this.disabled) if (!this.disabled)
this.checked = !this.checked this.checked = !this.checked
}); });
updateButtons();
}); });
['.editCheck', '.updateCheck', '.refreshCheck', '.renameCheck', '.deleteCheck', '.removeCheck'].forEach(function(name) { ['.edit-check', '.update-check', '.refresh-check', '.rename-check', '.delete-check', '.remove-check'].forEach(function(name){
var lastCheck = null; var lastCheck = null;
$(name).click(function(event) { $(name).click(function(event){
if(!lastCheck || !event.shiftKey){
if(!lastCheck || !event.shiftKey) {
lastCheck = this; lastCheck = this;
return;
} else {
var check = this, found = 0;
$(name).each(function(){
switch (found){
case 2:
return !1;
case 1:
if (!this.disabled)
this.checked = lastCheck.checked;
}
if (this === check || this === lastCheck)
found++;
});
} }
updateButtons();
});
});
var check = this, found = 0; $('#edit-check, .edit-check').each(function(i, el$){
$(el$).parent().attr('title', 'multiselect = ctrl/shift + left click');
})
$.SickGear.onRefreshSize = !1;
function presortTable(table$){
var asc = $('.sort-size.tablesorter-headerAsc').length,
desc = $('.sort-size.tablesorter-headerDesc').length,
value;
if (!!(asc + desc) || !!$.SickGear.onRefreshSize){
$('[data-size]').each(function(i, el){
value = parseInt($(el).attr('data-size'), 10);
$(el).attr('data-size', (!!desc !== !!$.SickGear.onRefreshSize) /* xor */
? ((value < 0) ? (-1 * value) + $.SickGear.high : value)
: ((value > $.SickGear.high) ? -1 * (value - $.SickGear.high) : value))
});
table$.trigger('updateCache');
}
}
$('#bulk-change-table')
.bind('refreshComplete', function(){
$.SickGear.onRefreshSize = !0;
})
.bind('sortEnd', function(){
$.SickGear.onRefreshSize = !1;
var el$ = $('.sort-size-type');
if (!!($('.sort-size.tablesorter-headerAsc').length + $('.sort-size.tablesorter-headerDesc').length)){
el$.removeClass('tablesorter-headerUnSorted').addClass('tablesorter-headerSorted');
} else {
el$.removeClass('tablesorter-headerSorted');
}
})
.bind('sortStart', function(e, t){
presortTable($(t));
})
.bind('filterInit filterEnd', function(e, data){
$('#tfoot').find('.stats').html(
(data.filteredRows === data.totalRows
? '<span class="total-rows">' + data.totalRows + '</span> shows'
: '<span class="filter-rows">' + data.filteredRows + '</span> of <span class="total-rows">' + data.totalRows + '</span> shows (' + (data.totalRows - data.filteredRows) + ' filtered)'));
});
$(name).each(function() { $('.sort-size-type').on('click', function(){
switch (found) { $('.sort-size-type-body').hide();
case 2: $('.sort-size-type-image').show();
return !1; var that = $(this), title = 'total', htmlType = '&Sigma;', dataType = 'E', key = 'Size';
case 1: if(dataType === $(this).attr('data-type')){
if (!this.disabled) title = 'average';
this.checked = lastCheck.checked; htmlType = '<div class="average"><i>x</i></div>';
dataType = 'x';
key = 'AverageSize';
} else if('x' === $(this).attr('data-type')){
title = 'largest';
htmlType = '&gt';
dataType = '>';
key = 'Largest';
} else if('>' === $(this).attr('data-type')){
title = 'smallest';
htmlType = '&lt';
dataType = '<';
key = 'Smallest';
}
var url = $.SickGear.Root + '/home/media_stats';
$.getJSON(url).then(function(content){
// on success...
var html, val, el$;
$.each(content, function(tvidProdid, data){
html = '---';
val = '-10';
el$ = $('tr[data-tvid_prodid="' + tvidProdid + '"][data-size]');
if (/undefined/.test(data.message) || 'E' === dataType){
html = data['h' + key];
val = data['b' + key];
} }
el$.find('.ui-size').html(html);
if (this == check || this == lastCheck) el$.attr('data-size', val);
found++;
}); });
that.attr('title', title);
that.attr('data-type', dataType);
that.find('.sort-size-type-body').html(htmlType);
$('#bulk-change-table').trigger('updateAll', [!0]);
$('.sort-size-type-image').hide();
$('.sort-size-type-body').show();
}, function(xhr, status, error){
// on failure...
console.log('data fetch error', url, status + ': ' + error);
$('.sort-size-type-image').hide();
$('.sort-size-type-body').show();
}); });
}); });

50
gui/slick/js/inc_top.js

@ -1,3 +1,11 @@
/** @namespace $.SickGear.Root */
/** @namespace content.path */
/** @namespace content.hSize */
/** @namespace content.nFiles */
/** @namespace content.hLargest */
/** @namespace content.hSmallest */
/** @namespace content.hAverageSize */
function initActions() { function initActions() {
var menu$ = $('#SubMenu'); var menu$ = $('#SubMenu');
menu$.find('a[href*="/home/restart/"]').addClass('btn restart').html('<i class="sgicon-restart"></i>Restart'); menu$.find('a[href*="/home/restart/"]').addClass('btn restart').html('<i class="sgicon-restart"></i>Restart');
@ -99,4 +107,46 @@ $(function(){
}); });
}); });
}) })
$('[data-size] .ui-size, #data-size').each(function(){
$(this).qtip({
content:{
text: function(event, api){
// deferred object ensuring the request is only made once
var tvidProdid = $('#tvid-prodid').val(), tipText = '';
if (/undefined/i.test(tvidProdid)){
tvidProdid = $(event.currentTarget).closest('[data-tvid_prodid]').attr('data-tvid_prodid');
}
$.getJSON($.SickGear.Root + '/home/media_stats', {tvid_prodid: tvidProdid})
.then(function(content){
// on success...
if (/undefined/.test(content[tvidProdid].message)){
tipText = (1 === content[tvidProdid].nFiles
? '<span class="grey-text">One media file,</span> ' + content[tvidProdid].hAverageSize
: '' + content[tvidProdid].nFiles + ' <span class="grey-text">media files,</span>'
+ ((content[tvidProdid].hLargest === content[tvidProdid].hSmallest)
? (content[tvidProdid].hAverageSize + ' <span class="grey-text">each</span>')
: ('<br><span class="grey-text">largest</span> ' + content[tvidProdid].hLargest + ' <span class="grey-text">(<span class="tip">></span>)</span>'
+ '<br><span class="grey-text">smallest</span> ' + content[tvidProdid].hSmallest + ' <span class="grey-text">(<span class="tip"><</span>)</span>'
+ '<br><span class="grey-text">average size</span> ' + content[tvidProdid].hAverageSize + ' <span class="grey-text">(<span class="tip-average"><i>x</i></span>)</span>')));
} else {
tipText = '<span class="grey-text">' + content[tvidProdid].message + '</span>';
}
api.set('content.text',
tipText + '<div style="width:100%; border-top:1px dotted; margin-top:3px"></div>'
+ '<div style="margin-top:3px">' + '<span class="grey-text">location size</span> ' + content[tvidProdid].hSize + ' <span class="grey-text">(<span class="tip">&Sigma;</span>)</span>' + '</div>'
+ content[tvidProdid].path);
}, function(xhr, status, error){
// on fail...
api.set('content.text', status + ': ' + error);
});
return 'Loading...'; // set initial text
}
},
show: {solo: true},
position: {viewport: $(window), my: 'left center', adjust: {y: -10, x: 0}},
style: {classes: 'qtip-dark qtip-rounded qtip-shadow'}
});
});
}); });

22
sickbeard/helpers.py

@ -1168,6 +1168,28 @@ def get_size(start_path='.'):
return 0 return 0
def get_media_stats(start_path='.'):
# type: (AnyStr) -> Tuple[int, int, int, int]
"""
return recognised media stats for a folder as...
number of media files, smallest size in bytes, largest size in bytes, average size in bytes
:param start_path: path to scan
"""
if ek.ek(os.path.isdir, start_path):
sizes = sorted(map(lambda y: y.stat(follow_symlinks=False).st_size,
filter(lambda x: has_media_ext(x.name), scantree(start_path))))
if sizes:
return len(sizes), sizes[0], sizes[-1], int(sum(sizes) / len(sizes))
elif ek.ek(os.path.isfile, start_path):
size = ek.ek(os.path.getsize, start_path)
return 1, size, size, size
return 0, 0, 0, 0
def remove_article(text=''): def remove_article(text=''):
""" """
remove articles from text remove articles from text

6
sickbeard/tv.py

@ -206,6 +206,9 @@ class TVidProdid(object):
class TVShow(TVShowBase): class TVShow(TVShowBase):
__slots__ = (
'path',
)
def __init__(self, tvid, prodid, lang=''): def __init__(self, tvid, prodid, lang=''):
# type: (int, int, Text) -> None # type: (int, int, Text) -> None
@ -219,7 +222,7 @@ class TVShow(TVShowBase):
self._not_found_count = None # type: None or int self._not_found_count = None # type: None or int
self._last_found_on_indexer = -1 # type: int self._last_found_on_indexer = -1 # type: int
self._location = '' # type: AnyStr self._location = self.path = '' # type: AnyStr
# self._is_location_good = None # self._is_location_good = None
self.lock = threading.Lock() self.lock = threading.Lock()
self.sxe_ep_obj = {} # type: Dict self.sxe_ep_obj = {} # type: Dict
@ -422,6 +425,7 @@ class TVShow(TVShowBase):
# Don't validate dir if user wants to add shows without creating a dir # Don't validate dir if user wants to add shows without creating a dir
if sickbeard.ADD_SHOWS_WO_DIR or ek.ek(os.path.isdir, new_location): if sickbeard.ADD_SHOWS_WO_DIR or ek.ek(os.path.isdir, new_location):
self.dirty_setter('_location')(self, new_location) self.dirty_setter('_location')(self, new_location)
self.path = new_location
# self._is_location_good = True # self._is_location_good = True
else: else:
raise exceptions_helper.NoNFOException('Invalid folder for the show!') raise exceptions_helper.NoNFOException('Invalid folder for the show!')

206
sickbeard/webserve.py

@ -54,7 +54,7 @@ from .anime import AniGroupList, pull_anidb_groups, short_group_names
from .browser import folders_at_path from .browser import folders_at_path
from .common import ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED, SNATCHED_ANY, UNAIRED, UNKNOWN, WANTED, \ from .common import ARCHIVED, DOWNLOADED, FAILED, IGNORED, SKIPPED, SNATCHED, SNATCHED_ANY, UNAIRED, UNKNOWN, WANTED, \
SD, HD720p, HD1080p, UHD2160p, Overview, Quality, qualityPresetStrings, statusStrings SD, HD720p, HD1080p, UHD2160p, Overview, Quality, qualityPresetStrings, statusStrings
from .helpers import has_image_ext, real_path, remove_article, remove_file_perm, starify from .helpers import get_media_stats, has_image_ext, real_path, remove_article, remove_file_perm, starify
from .indexermapper import MapStatus, map_indexers_to_show, save_mapping from .indexermapper import MapStatus, map_indexers_to_show, save_mapping
from .indexers.indexer_config import TVINFO_IMDB, TVINFO_TRAKT, TVINFO_TVDB from .indexers.indexer_config import TVINFO_IMDB, TVINFO_TRAKT, TVINFO_TVDB
from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser from .name_parser.parser import InvalidNameException, InvalidShowException, NameParser
@ -2301,6 +2301,32 @@ class Home(MainHandler):
return 'Episode not found.' if not sql_result else (sql_result[0]['description'] or '')[:250:] return 'Episode not found.' if not sql_result else (sql_result[0]['description'] or '')[:250:]
@staticmethod @staticmethod
def media_stats(tvid_prodid=None):
if None is tvid_prodid:
shows = sickbeard.showList
else:
shows = [helpers.find_show_by_id(tvid_prodid)]
response = {}
for cur_show_obj in shows:
if cur_show_obj.path:
loc_size = helpers.get_size(cur_show_obj.path)
num_files, smallest, largest, average_size = get_media_stats(cur_show_obj.path)
response[cur_show_obj.tvid_prodid] = {'message': 'No media files'} if not num_files else \
{
'nFiles': num_files,
'bSmallest': smallest, 'hSmallest': helpers.human(smallest),
'bLargest': largest, 'hLargest': helpers.human(largest),
'bAverageSize': average_size, 'hAverageSize': helpers.human(average_size)
}
response[cur_show_obj.tvid_prodid].update({
'path': cur_show_obj.path, 'bSize': loc_size, 'hSize': helpers.human(loc_size)})
return json.dumps(response)
@staticmethod
def scene_exceptions(tvid_prodid, wanted_season=None): def scene_exceptions(tvid_prodid, wanted_season=None):
exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(tvid_prodid) exceptionsList = sickbeard.scene_exceptions.get_all_scene_exceptions(tvid_prodid)
@ -5354,6 +5380,21 @@ class Manage(MainHandler):
def index(self): def index(self):
t = PageTemplate(web_handler=self, file='manage.tmpl') t = PageTemplate(web_handler=self, file='manage.tmpl')
t.submenu = self.manage_menu('Bulk') t.submenu = self.manage_menu('Bulk')
t.has_any_sports = False
t.has_any_anime = False
t.has_any_flat_folders = False
t.shows = []
t.shows_no_loc = []
for cur_show_obj in sorted(sickbeard.showList, key=lambda _x: _x.name.lower()):
t.has_any_sports |= bool(cur_show_obj.sports)
t.has_any_anime |= bool(cur_show_obj.anime)
t.has_any_flat_folders |= bool(cur_show_obj.flatten_folders)
if not cur_show_obj.path:
t.shows_no_loc += [cur_show_obj]
else:
t.shows += [cur_show_obj]
return t.respond() return t.respond()
def get_status_episodes(self, tvid_prodid, which_status): def get_status_episodes(self, tvid_prodid, which_status):
@ -6058,126 +6099,55 @@ class Manage(MainHandler):
self.redirect('/manage/') self.redirect('/manage/')
def bulk_change(self, to_update=None, to_refresh=None, def bulk_change(self, to_update='', to_refresh='', to_rename='',
to_rename=None, to_delete=None, to_remove=None, to_subtitle='', to_delete='', to_remove='', **kwargs):
to_metadata=None, to_subtitle=None):
if None is not to_update:
to_update = to_update.split('|')
else:
to_update = []
if None is not to_refresh:
to_refresh = to_refresh.split('|')
else:
to_refresh = []
if None is not to_rename:
to_rename = to_rename.split('|')
else:
to_rename = []
if None is not to_delete:
to_delete = to_delete.split('|')
else:
to_delete = []
if None is not to_remove:
to_remove = to_remove.split('|')
else:
to_remove = []
if None is not to_metadata:
to_metadata = to_metadata.split('|')
else:
to_metadata = []
if None is not to_subtitle:
to_subtitle = to_subtitle.split('|')
else:
to_subtitle = []
errors = []
updates = []
refreshes = []
renames = []
subs = []
for cur_tvid_prodid in set(to_update + to_refresh to_change = dict({_tvid_prodid: helpers.find_show_by_id(_tvid_prodid)
+ to_rename + to_delete + to_remove for _tvid_prodid in
+ to_metadata + to_subtitle): next(iter([_x.split('|') for _x in (to_update, to_refresh, to_rename, to_subtitle,
to_delete, to_remove) if _x]), '')})
if '' == cur_tvid_prodid: update, refresh, rename, subtitle, errors = [], [], [], [], []
continue for cur_tvid_prodid, cur_show_obj in iteritems(to_change):
show_obj = helpers.find_show_by_id(cur_tvid_prodid)
if None is show_obj:
continue
if cur_tvid_prodid in to_delete: if cur_tvid_prodid in to_delete:
show_obj.delete_show(True) cur_show_obj.delete_show(True)
# don't do anything else if it's being deleted
continue
if cur_tvid_prodid in to_remove:
show_obj.delete_show()
# don't do anything else if it's being remove
continue
if cur_tvid_prodid in to_update:
try:
sickbeard.show_queue_scheduler.action.updateShow(show_obj, True, True)
updates.append(show_obj.name)
except exceptions_helper.CantUpdateException as e:
errors.append('Unable to update show ' + show_obj.name + ': ' + ex(e))
# don't bother refreshing shows that were updated anyway elif cur_tvid_prodid in to_remove:
if cur_tvid_prodid in to_refresh and cur_tvid_prodid not in to_update: cur_show_obj.delete_show()
try:
sickbeard.show_queue_scheduler.action.refreshShow(show_obj)
refreshes.append(show_obj.name)
except exceptions_helper.CantRefreshException as e:
errors.append('Unable to refresh show ' + show_obj.name + ': ' + ex(e))
if cur_tvid_prodid in to_rename: else:
sickbeard.show_queue_scheduler.action.renameShowEpisodes(show_obj) if cur_tvid_prodid in to_update:
renames.append(show_obj.name) try:
sickbeard.show_queue_scheduler.action.updateShow(cur_show_obj, True, True)
if sickbeard.USE_SUBTITLES and cur_tvid_prodid in to_subtitle: update.append(cur_show_obj.name)
sickbeard.show_queue_scheduler.action.download_subtitles(show_obj) except exceptions_helper.CantUpdateException as e:
subs.append(show_obj.name) errors.append('Unable to update show %s: %s' % (cur_show_obj.name, ex(e)))
if 0 < len(errors):
ui.notifications.error('Errors encountered',
'<br >\n'.join(errors))
messageDetail = ''
if 0 < len(updates):
messageDetail += '<br /><b>Updates</b><br /><ul><li>'
messageDetail += '</li><li>'.join(updates)
messageDetail += '</li></ul>'
if 0 < len(refreshes): elif cur_tvid_prodid in to_refresh:
messageDetail += '<br /><b>Refreshes</b><br /><ul><li>' try:
messageDetail += '</li><li>'.join(refreshes) sickbeard.show_queue_scheduler.action.refreshShow(cur_show_obj)
messageDetail += '</li></ul>' refresh.append(cur_show_obj.name)
except exceptions_helper.CantRefreshException as e:
errors.append('Unable to refresh show %s: %s' % (cur_show_obj.name, ex(e)))
if 0 < len(renames): if cur_tvid_prodid in to_rename:
messageDetail += '<br /><b>Renames</b><br /><ul><li>' sickbeard.show_queue_scheduler.action.renameShowEpisodes(cur_show_obj)
messageDetail += '</li><li>'.join(renames) rename.append(cur_show_obj.name)
messageDetail += '</li></ul>'
if 0 < len(subs): if sickbeard.USE_SUBTITLES and cur_tvid_prodid in to_subtitle:
messageDetail += '<br /><b>Subtitles</b><br /><ul><li>' sickbeard.show_queue_scheduler.action.download_subtitles(cur_show_obj)
messageDetail += '</li><li>'.join(subs) subtitle.append(cur_show_obj.name)
messageDetail += '</li></ul>'
if 0 < len(updates + refreshes + renames + subs): if len(errors):
ui.notifications.message('The following actions were queued:', ui.notifications.error('Errors encountered', '<br>\n'.join(errors))
messageDetail)
if len(update + refresh + rename + subtitle):
ui.notifications.message(
'Queued the following actions:',
''.join(['%s:<br>* %s<br>' % (_to_do, '<br>'.join(_shows))
for (_to_do, _shows) in (('Updates', update), ('Refreshes', refresh),
('Renames', rename), ('Subtitles', subtitle)) if len(_shows)]))
self.redirect('/manage/') self.redirect('/manage/')
def failed_downloads(self, limit=100, to_remove=None): def failed_downloads(self, limit=100, to_remove=None):
@ -7341,7 +7311,7 @@ class ConfigGeneral(Config):
for v in results: for v in results:
logger.log(v, logger.ERROR) logger.log(v, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', ui.notifications.error('Error(s) Saving Configuration',
'<br />\n'.join(results)) '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -7511,7 +7481,7 @@ class ConfigSearch(Config):
for x in results: for x in results:
logger.log(x, logger.ERROR) logger.log(x, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', ui.notifications.error('Error(s) Saving Configuration',
'<br />\n'.join(results)) '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -7638,7 +7608,7 @@ class ConfigMediaProcess(Config):
for x in results: for x in results:
logger.log(x, logger.ERROR) logger.log(x, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', ui.notifications.error('Error(s) Saving Configuration',
'<br />\n'.join(results)) '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -8051,7 +8021,7 @@ class ConfigProviders(Config):
if 0 < len(results): if 0 < len(results):
for x in results: for x in results:
logger.log(x, logger.ERROR) logger.log(x, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', '<br />\n'.join(results)) ui.notifications.error('Error(s) Saving Configuration', '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -8319,7 +8289,7 @@ class ConfigNotifications(Config):
for x in results: for x in results:
logger.log(x, logger.ERROR) logger.log(x, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', ui.notifications.error('Error(s) Saving Configuration',
'<br />\n'.join(results)) '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -8374,7 +8344,7 @@ class ConfigSubtitles(Config):
for x in results: for x in results:
logger.log(x, logger.ERROR) logger.log(x, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', ui.notifications.error('Error(s) Saving Configuration',
'<br />\n'.join(results)) '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -8407,7 +8377,7 @@ class ConfigAnime(Config):
for x in results: for x in results:
logger.log(x, logger.ERROR) logger.log(x, logger.ERROR)
ui.notifications.error('Error(s) Saving Configuration', ui.notifications.error('Error(s) Saving Configuration',
'<br />\n'.join(results)) '<br>\n'.join(results))
else: else:
ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE))
@ -8524,7 +8494,7 @@ class EventLogs(MainHandler):
final_data += ['<code><span class="prelight">'] + \ final_data += ['<code><span class="prelight">'] + \
['<span class="prelight-num">%02s)</span> %s' % (n + 1, x) ['<span class="prelight-num">%02s)</span> %s' % (n + 1, x)
for n, x in enumerate(normal_data[::-1])] + \ for n, x in enumerate(normal_data[::-1])] + \
['</span></code><br />'] ['</span></code><br>']
num_lines += len(normal_data) num_lines += len(normal_data)
normal_data = [] normal_data = []

Loading…
Cancel
Save