Browse Source

Merge pull request #127 from oznzb-dev/0.7.x

0.7.x ratings, feedback and comments
tags/0.7.17Beta1
shypike 12 years ago
parent
commit
38c13bc4f0
  1. 6
      CHANGELOG.txt
  2. 1
      COPYRIGHT.txt
  3. 32
      README.mkd
  4. 61
      interfaces/Classic/templates/history.tmpl
  5. 9
      interfaces/Classic/templates/static/stylesheets/defaultcolors.css
  6. 1
      interfaces/Config/templates/config.tmpl
  7. 27
      interfaces/Config/templates/config_switches.tmpl
  8. 1
      interfaces/Plush/templates/_inc_header.tmpl
  9. 63
      interfaces/Plush/templates/_inc_modals.tmpl
  10. 40
      interfaces/Plush/templates/history.tmpl
  11. 19
      interfaces/Plush/templates/queue.tmpl
  12. 13
      interfaces/Plush/templates/static/javascripts/lib.js
  13. 62
      interfaces/Plush/templates/static/javascripts/plush.js
  14. 74
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/gold.css
  15. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/sound16.png
  16. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbdown20.png
  17. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbup20.png
  18. BIN
      interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/vision16.png
  19. BIN
      interfaces/Plush/templates/static/stylesheets/rateit/delete.gif
  20. 98
      interfaces/Plush/templates/static/stylesheets/rateit/rateit.css
  21. BIN
      interfaces/Plush/templates/static/stylesheets/rateit/star.gif
  22. 34
      interfaces/wizard/four.html
  23. 50
      interfaces/wizard/three.html
  24. 16
      sabnzbd/__init__.py
  25. 43
      sabnzbd/api.py
  26. 5
      sabnzbd/cfg.py
  27. 2
      sabnzbd/dirscanner.py
  28. 3
      sabnzbd/downloader.py
  29. 38
      sabnzbd/interface.py
  30. 220
      sabnzbd/misc.py
  31. 16
      sabnzbd/nzbstuff.py
  32. 10
      sabnzbd/postproc.py
  33. 261
      sabnzbd/rating.py
  34. 26
      sabnzbd/skintext.py
  35. 29
      sabnzbd/wizard.py

6
CHANGELOG.txt

@ -1,4 +1,10 @@
-------------------------------------------------------------------------------
0.7.17Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Add OZnzb features need to be enabled in config ->switches
- Add integration with OZnzb indexer enhanced functionality, allows user access to ratings and reporting directly from SABnzbd interface.
- Add automatic feedback to OZnzb on failed downloads (if enabled)
-------------------------------------------------------------------------------
0.7.16Final by The SABnzbd-Team
-------------------------------------------------------------------------------
- Fix Config->Special UI crash

1
COPYRIGHT.txt

@ -7,6 +7,7 @@ Active team:
ShyPike <shypike@sabnzbd.org>
inpheaux <inpheaux@sabnzbd.org>
zoggy <zoggy@sabnzbd.org>
OZnzb-dev <sabdev@oznzb.com>
Sleeping members
sw1tch <switch@sabnzbd.org>
pairofdimes <pairofdimes@sabnzbd.org>

32
README.mkd

@ -1,36 +1,12 @@
Release Notes - SABnzbd 0.7.16
Release Notes - SABnzbd 0.7.17
================================
## Bug fixes
- Fix false encryption alarms for some posts
- Fix for faulty par2cmdline on some embbeded Unix systems
## Features
- Add "password" box to Plush's job details page
- Add special "sanitize_safe" to remove unsupported Windows characters on other platforms.
This solves issues when using NAS shares from Windows.
## What's new in 0.7.0
- Download quota management
- Windows: simple system tray menu
- Multi-platform Growl support
- NotifyOSD support for Linux distros that have it
- Option to set maximum number of retries for servers (prevents deadlock)
- Pre-download check to estimate completeness (reliability is limited)
- Prevent partial downloading of par2 files that are not needed yet
- Config->Special for settings previously only available in the sabnzbd.ini file
- For Usenet servers with multiple IP addresses, pick a random one per connection
- Add pseudo-priority "Stop" that will send the job immediately to the post-processing queue
- Allow jobs still waiting for post-processing to be deleted too
- More persistent retries for unreliable indexers
- Single Configuration skin for all others skins (there is an option for the old style)
- Config->Special for settings that were previously only changeable in the sabnzbd.ini file
- Add Spanish, Portuguese (Brazil) and Polish translations
- Individual RSS filter toggle
- Unified OSX DMG
- Add OZnzb new features need to be enabled in config ->switches
- Add integration with OZnzb indexer enhanced functionality, allows user access to ratings and reporting directly from SABnzbd interface.
- Add automatic feedback to OZnzb on failed downloads (if enabled)
## About

61
interfaces/Classic/templates/history.tmpl

@ -31,7 +31,15 @@ $T('thisWeek'): $week_size&nbsp;&nbsp;|&nbsp;&nbsp;$T('thisMonth'): $month_size
<% from sabnzbd.misc import time_format %>
<!--#if $lines#-->
<table id="historyTable">
<tr><th></th><th>$T('completed')</th><th>$T('name')</th><th>$T('size')</th><th>$T('status')</th><th></th></tr>
<tr>
<th></th>
<th>$T('completed')</th>
<th>$T('name')</th>
<th>$T('size')</th>
<th>$T('status')</th>
<!--#if $rating_enable#--><th>Rating</th><!--#end if#-->
<th></th>
</tr>
<!--#set $odd = False#-->
<!--#for $line in $lines #-->
<%
@ -44,7 +52,20 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
</a></td>
<td>$compl</td>
<td>$line.name<!--#if $line.action_line#--> - $line.action_line<!--#else if $line.fail_message#--> - <span class="fail_message">$line.fail_message</span><!--#end if#--></td>
<td>$line.size</td><td>$Tx('post-'+$line.status)</td>
<td>$line.size</td>
<td>$Tx('post-'+$line.status)</td>
<!--#if $rating_enable#-->
<!--#if $line.has_rating#-->
<td><div class="rating_overall">$T('video')&nbsp;$line.rating_avg_video $T('audio')&nbsp;$line.rating_avg_audio</div>
<form method="GET" action="./show_edit_rating">
<input type="hidden" name="job" value="$line.nzo_id">
<input type="hidden" name="session" value="$session">
<input type="submit" value="$T('report')">
</form></td>
<!--#else#-->
<td></td>
<!--#end if#-->
<!--#end if#-->
<td>
<!--#if not $line.loaded#-->
<!--#if $line.retry#-->
@ -66,6 +87,41 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
<!--#end if#-->
</td>
</tr>
<!--#if $line.edit_rating#-->
<!--#set $oddLine = not False#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#-->"><td></td><td></td>
<td colspan="3">
<form action="action_edit_rating" method="post" enctype="multipart/form-data">
<input type="hidden" value="$line.nzo_id" name="job">
<input type="hidden" value="$session" name="session" >
<div class="rating_item">$T('video')&nbsp;
<select name="video">
<!--#if not $line.rating_user_video#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option <!--#if $line.rating_user_video==$val#-->selected<!--#end if#--> >$val</option><!--#end for#-->
</select>
</div>
<div class="rating_item">$T('audio')&nbsp;
<select name="audio">
<!--#if not $line.rating_user_audio#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option <!--#if $line.rating_user_audio==$val#-->selected<!--#end if#--> >$val</option><!--#end for#-->
</select>
</div>
<div class="rating_item">
<input type="radio" name="rating_flag" value="spam">&nbsp;$T('spam')
<input type="radio" name="rating_flag" value="encrypted">&nbsp;$T('encrypted')
<input type="radio" name="rating_flag" value="expired">&nbsp;$T('expired')
<input type="text" name="expired_host" style="margin-left:10px" value="<$T('host')>">
</div>
<div class="rating_item">
<input type="submit" name="send" value="$T('send')">
<input type="submit" name="cancel" value="$T('cancel')">
</div>
</form>
</td>
<td></td>
<td></td>
</tr>
<!--#end if#-->
<!--#if $line.show_details#-->
<!--#set $oddLine = not False#-->
<tr class="<!--#if $oddLine then "oddLine" else "evenLine"#-->"><td></td><td></td>
@ -91,6 +147,7 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
</dl>
</td>
<td></td>
<!--#if $rating_enable#--><td></td><!--#end if#-->
</tr>
<!--#end if#-->
<!--#end for#-->

9
interfaces/Classic/templates/static/stylesheets/defaultcolors.css

@ -136,3 +136,12 @@ color:black;
.feedEnabled{color:green;}
.feedDisabled{color:red;}
.rating_overall {
margin:0px 5px 3px 0px;
}
.rating_item {
float:left;
margin:5px 15px 5px 5px;
}

1
interfaces/Config/templates/config.tmpl

@ -17,6 +17,7 @@
<tr class="alt"><td class="infoTableHeader">$T('menu-forums') </td><td class="infoTableCell"><a href="http://forums.sabnzbd.org/" target="_blank">http://forums.sabnzbd.org/</a></td></tr>
<tr><td class="infoTableHeader">$T('source') </td><td class="infoTableCell"><a href="https://github.com/sabnzbd/sabnzbd" target="_blank">https://github.com/sabnzbd/sabnzbd</a></td></tr>
<tr class="alt"><td class="infoTableHeader">$T('menu-irc') </td><td class="infoTableCell"><a href="irc://irc.synirc.net/#sabnzbd"><i>#sabnzbd</i> on <i>irc.synirc.net</i></a> $T('or') (<a href="http://sabnzbd.org/live-chat/" target="_blank">webchat</a>)</td></tr>
<tr><td class="infoTableHeader">$T('oznzb')</td><td class="infoTableCell"><a href="https://www.oznzb.com/register" target="_blank">https://www.oznzb.com/register</a></td></tr>
</tbody>
</table>
</div>

27
interfaces/Config/templates/config_switches.tmpl

@ -305,6 +305,33 @@
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="section">
<div class="col2">
<h3>$T('swtag-indexing')</h3>
</div><!-- /col2 -->
<div class="col1">
<fieldset>
<div class="field-pair">
<label class="config" for="rating_enable">$T('opt-rating_enable')</label>
<input type="checkbox" name="rating_enable" id="rating_enable" value="1" <!--#if int($rating_enable) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_enable')</span>
</div>
<div class="field-pair alt">
<label class="config" for="rating_api_key">$T('opt-rating_api_key')</label>
<input type="text" name="rating_api_key" id="rating_api_key" value="$rating_api_key" size="35" />
<span class="desc">$T('explain-rating_api_key')</span>
</div>
<div class="field-pair">
<label class="config" for="rating_feedback">$T('opt-rating_feedback')</label>
<input type="checkbox" name="rating_feedback" id="rating_feedback" value="1" <!--#if int($rating_feedback) > 0 then 'checked="checked"' else ""#--> />
<span class="desc">$T('explain-rating_feedback')</span>
</div>
<div class="field-pair">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
</div>
</fieldset>
</div><!-- /col1 -->
</div><!-- /section -->
<div class="padding alt">
<input type="submit" value="$T('button-saveChanges')" class="saveButton" />
<input type="button" value="$T('button-restart') SABnzbd" class="sabnzbd_restart" />

1
interfaces/Plush/templates/_inc_header.tmpl

@ -14,6 +14,7 @@
<link rel="alternate" type="application/rss+xml" title="RSS 2.0" href="${path}rss?mode=history"/>
<link rel="stylesheet" type="text/css" href="${path}static/stylesheets/jqueryui/overcast/jquery-ui-1.8.15.custom.css?$version"/>
<link rel="stylesheet" type="text/css" href="${path}static/stylesheets/rateit/rateit.css"/>
#if $color_scheme#
<link rel="shortcut icon" type="image/ico" href="${path}static/stylesheets/colorschemes/$color_scheme/images/sabnzbdplus.ico"/>
<link rel="stylesheet" type="text/css" href="${path}static/stylesheets/colorschemes/$color_scheme/${color_scheme}.css?$version"/>

63
interfaces/Plush/templates/_inc_modals.tmpl

@ -1,3 +1,21 @@
<script type="text/javascript">
function expired_host_changed(self) {
var host = document.getElementsByName('expired_host')[0];
host.value = self.value;
host.readOnly = self.value.length > 0;
}
function flag_modal_submit(self) {
var radios = document.getElementsByName('rating_flag');
for (var i = 0; i < radios.length; i++) {
if (radios[i].checked) {
document.getElementById('noopt').setAttribute('style', 'display:none;size:1');
document.getElementById('submitbtn').click();
return;
}
}
document.getElementById('noopt').removeAttribute('style');
}
</script>
<!-- modals -->
<div style='display:none'>
@ -142,6 +160,51 @@ $T('Plush-containerWidth'):
<input type="submit" id="delete_nzb_modal_remove_files" value="$T('removeNZB-Files')" class="juiButton" />
</div>
<div id="flag_modal">
<input type="hidden" id="flag_modal_job" />
<div class="rating_flag_radio"><input type="radio" name="rating_flag" value="spam">&nbsp;$T('spam')</div>
<div class="rating_flag_radio"><input type="radio" name="rating_flag" value="encrypted">&nbsp;$T('encrypted')</div>
<div class="rating_flag_radio">
<input type="radio" name="rating_flag" value="expired">&nbsp;$T('expired')
<div class="rating_modal_extra">
<div class="rating_modal_expired">$T('host')&nbsp;&nbsp;<input type="text" name="expired_host" value="www.altopia.com" readonly></div>
<select name="common_host" onchange="expired_host_changed(this)">
<option value='www.altopia.com' selected>Altopia</option>
<option value='www.astraweb.com'>Astraweb</option>
<option value='www.euroaccess.ln'>EuroAccess</option>
<option value='www.forteinc.com'>Forte Agent</option>
<option value='www.giganews.com'>Giganews</option>
<option value='www.highwinds.com'>Highwinds</option>
<option value='www.newsdemon.com'>Newsdemon</option>
<option value='www.newsgroupdirect.com'>NewsGroupDirect</option>
<option value='www.newshosting.com'>NewsHosting</option>
<option value='www.readnews.com'>Readnews</option>
<option value='www.supernews.com'>SuperNews</option>
<option value='www.thundernews.com'>ThunderNews</option>
<option value='www.tweaknews.eu'>Tweaknews</option>
<option value='www.usenetserver.com'>UsenetServer</option>
<option value='www.xentech.net'>XenTech</option>
<option value='www.xsnews.nl'>XSnews</option>
<option value=''>$T('other')</option>
</select>
</div>
</div>
<div class="rating_flag_radio">
<input type="radio" name="rating_flag" value="other">&nbsp;$T('otherProblem')
<div class="rating_modal_extra"><input style="width:99%" type="text" name="other"></div>
</div>
<div class="rating_flag_radio">
<input type="radio" name="rating_flag" value="comment">&nbsp;$T('comment')
<div class="rating_modal_extra"><input style="width:99%" type="text" name="comment"></div>
</div>
<br/>
<div class="center">
<input id="submitbtn" type="submit" style="display:none;size:1"/>
<input value="Send" class="juiButton" onclick="flag_modal_submit(this)"/>
<label id="noopt" class="rating_modal_noopt" style="display:none;size:1">No option selected</label>
</div>
</div>
#end if#
</div>

40
interfaces/Plush/templates/history.tmpl

@ -24,7 +24,7 @@
<td class="nzb_status_col">
&nbsp;<div class="nzb_status <!--#if $line.action_line or $line.status=="Queued"#-->Loaded<!--#else if $line.status=="Failed"#-->main_sprite_container sprite_hv_error<!--#else#-->main_sprite_container sprite_hv_star<!--#end if#-->">&nbsp;</div>
</td>
<td class="historyTitle">
<td class="historyTitle" <!--#if $rating_enable#-->style="width:35%"<!--#end if#-->>
<a href="scriptlog?name=$line.nzo_id" class="modal-detail" rel="details">$line.name</a>
<div style="display:none">
@ -106,6 +106,44 @@
<!--#end if#-->
</td>
<!--#if $rating_enable#-->
<!--#if $line.has_rating#-->
<td>
<div class="rating_stars_block_r">
<div class="rating_stars">
<div class="rating_icon_vision"></div><span class="avg_rate" value="$line.rating_avg_video"></span>
<input class="user_combo" type="hidden" value="$line.rating_user_video">
<select class="user_combo video" style="background:transparent">
<!--#if not $line.rating_user_video#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option>$val</option><!--#end for#-->
</select>
</div>
<div class="rating_stars">
<div class="rating_icon_sound"></div><span class="avg_rate" value="$line.rating_avg_audio"></span>
<input class="user_combo" type="hidden" value="$line.rating_user_audio">
<select class="user_combo audio" style="background:transparent">
<!--#if not $line.rating_user_audio#--><option>-</option><!--#end if#-->
<!--#for $val in $range(1, 11)#--><option>$val</option><!--#end for#-->
</select>
</div>
</div>
</td>
<td>
<div class="rating_vote_block">
<div class="rating_icon_thumbup user_vote up"></div>
<!--#if $line.rating_user_vote==1#--><b><!--#end if#-->$line.rating_avg_vote_up<!--#if $line.rating_user_vote==1#--></b><!--#end if#-->
<div class="rating_icon_thumbdown user_vote down"></div>
<!--#if $line.rating_user_vote==2#--><b><!--#end if#-->$line.rating_avg_vote_down<!--#if $line.rating_user_vote==2#--></b><!--#end if#-->
</div>
<div class="rating_flag">
<a href="#" class="show_flags">$T('report')</a>
</div>
</td>
<!--#else#-->
<td></td><td></td>
<!--#end if#-->
<!--#end if#-->
<td class="options nowrap">
<!--#if not $line.loaded#-->
<% d = datetime.datetime.fromtimestamp(float(line['completed'])) %>

19
interfaces/Plush/templates/queue.tmpl

@ -55,10 +55,27 @@
<% # <!--#else if $slot.status == "Downloading"#-->main_sprite_container sprite_ql_grip_active %>
</td>
<td class="download-title">
<td class="download-title" <!--#if $rating_enable#-->style="width:35%"<!--#end if#-->>
<a href="nzb/$slot.nzo_id/" title="$T('status'): $T('post-'+$slot.status)<br/>$T('nzo-age'): $slot.avg_age<br/><!--#if $slot.missing#-->$T('missingArt'): $slot.missing<!--#end if#-->">$slot.filename.replace('.', '.&#8203;').replace('_', '_&#8203;')</a>
</td>
<!--#if $rating_enable#-->
<!--#if $slot.has_rating#-->
<td>
<div class="rating_stars_block_c">
<div class="rating_stars">
<div class="rating_icon_vision"></div><span class="avg_rate" value="$slot.rating_avg_video"></span>
</div>
<div class="rating_stars">
<div class="rating_icon_sound"></div><span class="avg_rate" value="$slot.rating_avg_audio"></span>
</div>
</div>
</td>
<!--#else#-->
<td></td>
<!--#end if#-->
<!--#end if#-->
<td>
<div class="main_sprite_container sprite_progressbar_bg">
<div class="main_sprite_container sprite_progress_done" style="background-position: -<!--#if $slot.mb == "0.00" then "120" else int(120 - 120.0 / 100.0 * int(100 - float($slot.mbleft) / float($slot.mb) * 100))#-->px -401px">

13
interfaces/Plush/templates/static/javascripts/lib.js

File diff suppressed because one or more lines are too long

62
interfaces/Plush/templates/static/javascripts/plush.js

@ -1047,6 +1047,12 @@ $("a","#multiops_inputs").click(function(e){
title:function(){return $(this).text();},
innerWidth:"80%", innerHeight:"300px", initialWidth:"80%", initialHeight:"300px", speed:0, opacity:0.7 });
// modal for reporting issues
$("#historyTable .modal-report").colorbox({ inline:true,
href: function(){return "#report-"+$(this).parent().parent().parent().attr('id');},
title:function(){return $(this).text();},
innerWidth:"250px", innerHeight:"110px", initialWidth:"250px", initialHeight:"110px", speed:0, opacity:0.7 });
// Build pagination only when needed
if ($.plush.histPerPage=="1") // disabled history
$("#history-pagination").html(''); // remove pages if history empty
@ -1073,6 +1079,55 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
}); // end livequery
$('.user_combo').livequery('change', function(){
var nzo_id = $(this).parent().parent().parent().parent().attr('id');
var videoAudio = $(this).hasClass('video') ? 'video' : 'audio';
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'rating', value: nzo_id, type: videoAudio, setting: $(this).val(), apikey: $.plush.apikey},
success: $.plush.RefreshHistory
});
});
$('.user_vote').livequery('click', function(){
var nzo_id = $(this).parent().parent().parent().attr('id');
var upDown = $(this).hasClass('up') ? 'up' : 'down';
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'rating', value: nzo_id, type: 'vote', setting: upDown, apikey: $.plush.apikey},
success: $.plush.RefreshHistory
});
});
$('#history .show_flags').live('click', function(){
$('#flag_modal_job').val( $(this).parent().parent().parent().attr('id') );
$.colorbox({ inline:true, href:"#flag_modal", title:$(this).text(),
innerWidth:"500px", innerHeight:"185px", initialWidth:"500px", initialHeight:"185px", speed:0, opacity:0.7
});
return false;
});
$('#flag_modal input:submit').click(function(){
var nzo_id = $('#flag_modal_job').val();
var flag = $('input[name=rating_flag]:checked', '#flag_modal').val();
var expired_host = $('input[name=expired_host]', '#flag_modal').val();
var other = $('input[name=other]', '#flag_modal').val();
var comment = $('input[name=comment]', '#flag_modal').val();
var _detail = (flag == 'comment') ? comment : ((flag == 'other') ? other : expired_host);
$.colorbox.close();
$.plush.modalOpen=false;
$.ajax({
headers: {"Cache-Control": "no-cache"},
type: "POST",
url: "tapi",
data: {mode:'queue', name:'rating', value: nzo_id, type: 'flag', setting: flag, detail: _detail, apikey: $.plush.apikey},
success: $.plush.RefreshHistory
});
});
}, // end $.plush.InitHistory()
@ -1132,6 +1187,8 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
$('.left_stats .initial-loading').hide();
$('#queue').html(result); // Replace queue contents with queue.tmpl
$('#queue .avg_rate').rateit({readonly: true, resetable: false, step: 0.5});
$('#queue .avg_rate').each(function() { $(this).rateit('value', $(this).attr('value') / 2); });
if ($.plush.multiOps) // add checkboxes
$('<input type="checkbox" class="multiops" />').appendTo('#queue tr td.nzb_status_col');
@ -1187,6 +1244,11 @@ $.plush.histprevslots = $.plush.histnoofslots; // for the next refresh
}
$('.left_stats .initial-loading').hide();
$('#history').html(result); // Replace history contents with history.tmpl
$('#history .avg_rate').rateit({readonly: true, resetable: false, step: 0.5});
$('#history .avg_rate').each(function() { $(this).rateit('value', $(this).attr('value') / 2); });
$('#history .user_combo option').filter(function() {
return $(this).attr('value') == $(this).parent().parent().find('input.user_combo').attr('value');
}).attr('selected', true);
$('#history-pagination span').removeClass('loading'); // Remove spinner graphic from pagination
}
});

74
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/gold.css

@ -1073,7 +1073,81 @@ tr:hover .history_added { color: black; }
.pointer { cursor: pointer; }
/* ---------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------
Ratings
------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------- */
.rating_stars_block_r {
text-align: right;
}
.rating_stars_block_c {
text-align: center;
}
.rating_vote_block {
float: left;
}
.rating_stars {
display: inline-block;
margin-right: 10px;
}
.rating_flag {
margin: 4px 10px 0px 85px;
}
.rating_flag_radio {
margin: 5px 15px 5px 5px;
}
.rating_modal_extra {
float: right;
width: 330px
}
.rating_modal_expired {
float: right;
}
.rating_modal_noopt {
color: red;
margin-left: 10px;
}
.rating_icon_vision {
width: 16px;
height: 16px;
display: inline-block;
margin-right: 5px;
background: url('images/vision16.png') no-repeat top center;
}
.rating_icon_sound {
width: 16px;
height: 16px;
display: inline-block;
margin-right: 5px;
background: url('images/sound16.png') no-repeat top center;
}
.rating_icon_thumbup {
width: 20px;
height: 20px;
display: inline-block;
background: url('images/thumbup20.png') no-repeat top center;
}
.rating_icon_thumbdown {
width: 20px;
height: 20px;
display: inline-block;
background: url('images/thumbdown20.png') no-repeat top center;
}
/* ---------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/sound16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 B

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbdown20.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 B

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/thumbup20.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

BIN
interfaces/Plush/templates/static/stylesheets/colorschemes/gold/images/vision16.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 341 B

BIN
interfaces/Plush/templates/static/stylesheets/rateit/delete.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 752 B

98
interfaces/Plush/templates/static/stylesheets/rateit/rateit.css

@ -0,0 +1,98 @@
.rateit {
display: -moz-inline-box;
display: inline-block;
position: relative;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
-ms-user-select: none;
user-select: none;
-webkit-touch-callout: none;
}
.rateit .rateit-range
{
position: relative;
display: -moz-inline-box;
display: inline-block;
background: url(star.gif);
height: 16px;
outline: none;
}
.rateit .rateit-range * {
display:block;
}
/* for IE 6 */
* html .rateit, * html .rateit .rateit-range
{
display: inline;
}
/* for IE 7 */
* + html .rateit, * + html .rateit .rateit-range
{
display: inline;
}
.rateit .rateit-hover, .rateit .rateit-selected
{
position: absolute;
left: 0px;
}
.rateit .rateit-hover-rtl, .rateit .rateit-selected-rtl
{
left: auto;
right: 0px;
}
.rateit .rateit-hover
{
background: url(star.gif) left -32px;
}
.rateit .rateit-hover-rtl
{
background-position: right -32px;
}
.rateit .rateit-selected
{
background: url(star.gif) left -48px;
}
.rateit .rateit-selected-rtl
{
background-position: right -48px;
}
.rateit .rateit-preset
{
background: url(star.gif) left -16px;
}
.rateit .rateit-preset-rtl
{
background: url(star.gif) left -16px;
}
.rateit button.rateit-reset
{
background: url(delete.gif) 0 0;
width: 16px;
height: 16px;
display: -moz-inline-box;
display: inline-block;
float: left;
outline: none;
border:none;
padding: 0;
}
.rateit button.rateit-reset:hover, .rateit button.rateit-reset:focus
{
background-position: 0 -16px;
}

BIN
interfaces/Plush/templates/static/stylesheets/rateit/star.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

34
interfaces/wizard/four.html

@ -0,0 +1,34 @@
<!--#include $webdir + "/inc_top.tmpl"#-->
<script type="text/javascript" src="static/javascript/jquery.js"></script>
<script type="text/javascript" src="static/javascript/restart.js"></script>
<br/><br/>
<h4 id="restarting" class="align-center">$T('wizard-restarting')</h4>
<h4 id="complete" class="align-center success hidden">$T('wizard-complete')</h4>
<br />
<br/>
<div id="tips" class="hidden">
$T('wizard-tip1') <span class="bold">$T('wizard-tip2')</span><br/>
<!--#set $tip3 = $T('wizard-tip3') % ''#-->
$tip3<br/><br/>
<div class="quoteBlock">
<!--#set $i = 0#-->
<!--#for $url in $urls#-->
<!--#set $i = $i+1#-->
<a href="$url">$url</a><!--#if $i != len($urls)#--><br /><!--#end if#-->
<!--#end for#-->
</div><br/>
$T('wizard-tip4')
<br/><br/>
$T('wizard-tip-wiki') <a href="$helpuri">wiki</a>
</div>
</div>
<hr /><br/>
<div class="full-width">
<table class="full-width">
<tr class="align-center">
<td><input type="hidden" name="session" id="apikey" value="$session"><input class="bigbutton disabled" type="button" onclick="document.location ='$access_url'" value="$T('wizard-goto')" disabled="disabled"/></td>
</tr>
</table>
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

50
interfaces/wizard/three.html

@ -1,34 +1,38 @@
<!--#include $webdir + "/inc_top.tmpl"#-->
<script type="text/javascript" src="static/javascript/jquery.js"></script>
<script type="text/javascript" src="static/javascript/restart.js"></script>
<br/><br/>
<h4 id="restarting" class="align-center">$T('wizard-restarting')</h4>
<h4 id="complete" class="align-center success hidden">$T('wizard-complete')</h4>
<br />
<br/>
<div id="tips" class="hidden">
$T('wizard-tip1') <span class="bold">$T('wizard-tip2')</span><br/>
<!--#set $tip3 = $T('wizard-tip3') % ''#-->
$tip3<br/><br/>
<div class="quoteBlock">
<!--#set $i = 0#-->
<!--#for $url in $urls#-->
<!--#set $i = $i+1#-->
<a href="$url">$url</a><!--#if $i != len($urls)#--><br /><!--#end if#-->
<!--#end for#-->
</div><br/>
$T('wizard-tip4')
<br/><br/>
$T('wizard-tip-wiki') <a href="$helpuri">wiki</a>
<form action="./four" method="post" autocomplete="off">
<div class="indented bigger">
<h3>Indexer</h3>
<div>$T('explain-rating_enable')</div>
<div>$T('wizard-create-account')<a href="https://www.oznzb.com/register" target="_blank">https://www.oznzb.com/register</a>.</div>
<br class="clear" />
<input type="checkbox" name="rating_enable" id="rating_enable" value="1" <!--#if $rating_enable == 1 then 'checked="checked"' else ''#-->> <label for="rating_enable">$T('opt-rating_enable')</label><br />
<br class="clear" />
<div>
<label class="label">$T('opt-rating_api_key')</label><input type="text" size="35" value="$rating_api_key" name="rating_api_key" id="rating_api_key">
<div class="tips">$T('tip-rating_api_key')</div>
</div>
<br class="clear" />
</div>
</div>
<hr /><br/>
<div class="full-width">
<table class="full-width">
<tr class="align-center">
<td><input type="hidden" name="session" id="apikey" value="$session"><input class="bigbutton disabled" type="button" onclick="document.location ='$access_url'" value="$T('wizard-goto')" disabled="disabled"/></td>
<tr>
<td><input class="bigbutton" type="button" onclick="document.location ='./two'" value="&lsaquo; $T('wizard-previous')" /></td>
<td>
<div class="align-center">
<!--#for $step in xrange($steps)#-->
<!--#set $step = $step + 1#-->
<span class="<!--#if $step == $number then 'selected' else 'unselected'#-->">$step</span>
<!--#end for#-->
</div>
</td>
<td class="align-right"><input class="bigbutton" type="submit" value="$T('wizard-next') &raquo;" /></td>
</tr>
</table>
</div>
<!--#include $webdir + "/inc_bottom.tmpl"#-->
</form>
<!--#include $webdir + "/inc_bottom.tmpl"#-->

16
sabnzbd/__init__.py

@ -75,6 +75,7 @@ from sabnzbd.postproc import PostProcessor
from sabnzbd.downloader import Downloader
from sabnzbd.assembler import Assembler
from sabnzbd.newzbin import Bookmarks, MSGIDGrabber
from sabnzbd.rating import Rating
import sabnzbd.misc as misc
import sabnzbd.powersup as powersup
from sabnzbd.dirscanner import DirScanner, ProcessArchiveFile, ProcessSingleFile
@ -319,6 +320,8 @@ def initialize(pause_downloader = False, clean_up = False, evalSched=False, repa
DirScanner()
MSGIDGrabber()
Rating()
URLGrabber()
@ -354,6 +357,8 @@ def start():
MSGIDGrabber.do.start()
Rating.do.start()
logging.debug('Starting urlgrabber')
URLGrabber.do.start()
@ -384,6 +389,13 @@ def halt():
except:
pass
logging.debug('Stopping rating')
Rating.do.stop()
try:
Rating.do.join()
except:
pass
logging.debug('Stopping dirscanner')
DirScanner.do.stop()
try:
@ -509,6 +521,7 @@ def save_state(flag=False):
BPSMeter.do.save()
rss.save()
Bookmarks.do.save()
Rating.do.save()
DirScanner.do.save()
PostProcessor.do.save()
#if flag:
@ -1037,6 +1050,9 @@ def check_all_tasks():
if not MSGIDGrabber.do.isAlive():
logging.info('Restarting crashed newzbin')
MSGIDGrabber.do.__init__()
if not Rating.do.isAlive():
logging.info('Restarting crashed rating')
Rating.do.__init__()
if not sabnzbd.scheduler.sched_check():
logging.info('Restarting crashed scheduler')
sabnzbd.scheduler.init()

43
sabnzbd/api.py

@ -58,6 +58,7 @@ from sabnzbd.postproc import PostProcessor
from sabnzbd.articlecache import ArticleCache
from sabnzbd.utils.servertests import test_nntp_server_dict
from sabnzbd.newzbin import Bookmarks
from sabnzbd.rating import Rating
from sabnzbd.bpsmeter import BPSMeter
from sabnzbd.database import build_history_info, unpack_history_info, get_history_handle
import sabnzbd.growler
@ -270,6 +271,24 @@ def _api_queue_default(output, value, kwargs):
else:
return report(output, _MSG_NOT_IMPLEMENTED)
def _api_queue_rating(output, value, kwargs):
""" API: accepts output, value(=nzo_id), type, setting, detail """
vote_map = {'up': Rating.VOTE_UP, 'down': Rating.VOTE_DOWN}
flag_map = {'spam': Rating.FLAG_SPAM, 'encrypted': Rating.FLAG_ENCRYPTED, 'expired': Rating.FLAG_EXPIRED, 'other': Rating.FLAG_OTHER, 'comment': Rating.FLAG_COMMENT}
type = kwargs.get('type')
setting = kwargs.get('setting')
if value:
try:
video = setting if type == 'video' and setting != "-" else None
audio = setting if type == 'audio' and setting != "-" else None
vote = vote_map[setting] if type == 'vote' else None
flag = flag_map[setting] if type == 'flag' else None
Rating.do.update_user_rating(value, video, audio, vote, flag, kwargs.get('detail'))
return report(output)
except:
return report(output, _MSG_BAD_SERVER_PARMS)
else:
return report(output, _MSG_NO_VALUE)
#------------------------------------------------------------------------------
def _api_options(name, output, kwargs):
@ -819,7 +838,8 @@ _api_queue_table = {
'pause' : _api_queue_pause,
'resume' : _api_queue_resume,
'priority' : _api_queue_priority,
'sort' : _api_queue_sort
'sort' : _api_queue_sort,
'rating' : _api_queue_rating
}
_api_config_table = {
@ -1044,6 +1064,7 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
info['script_list'] = list_scripts()
info['cat_list'] = list_cats(output is None)
info['rating_enable'] = bool(cfg.rating_enable())
n = 0
found_active = False
@ -1213,8 +1234,13 @@ def build_queue(web_dir=None, root=None, verbose=False, prim=True, webdir='', ve
slot['finished'] = finished
slot['active'] = active
slot['queued'] = queued
rating = Rating.do.get_rating_by_nzo(nzo_id)
slot['has_rating'] = rating is not None
if rating:
slot['rating_avg_video'] = rating.avg_video
slot['rating_avg_audio'] = rating.avg_audio
if (start <= n and n < start + limit) or not limit:
slotinfo.append(slot)
n += 1
@ -1762,6 +1788,17 @@ def build_history(start=None, limit=None, verbose=False, verbose_list=None, sear
if item['retry']:
retry_folders.append(path)
rating = Rating.do.get_rating_by_nzo(item['nzo_id'])
item['has_rating'] = rating is not None
if rating:
item['rating_avg_video'] = rating.avg_video
item['rating_avg_audio'] = rating.avg_audio
item['rating_avg_vote_up'] = rating.avg_vote_up
item['rating_avg_vote_down'] = rating.avg_vote_down
item['rating_user_video'] = rating.user_video
item['rating_user_audio'] = rating.user_audio
item['rating_user_vote'] = rating.user_vote
total_items += full_queue_size
fetched_items = len(items)

5
sabnzbd/cfg.py

@ -113,6 +113,11 @@ newzbin_unbookmark = OptionBool('newzbin', 'unbookmark', True)
bookmark_rate = OptionNumber('newzbin', 'bookmark_rate', 60, minval=15, maxval=24*60)
newzbin_url = OptionStr('newzbin', 'url', 'www.newzbin2.es')
rating_enable = OptionBool('misc', 'rating_enable', False)
rating_host = OptionStr('misc', 'rating_host', 'www.oznzb.com')
rating_api_key = OptionStr('misc', 'rating_api_key')
rating_feedback = OptionBool('misc', 'rating_feedback', True)
top_only = OptionBool('misc', 'top_only', False)
autodisconnect = OptionBool('misc', 'auto_disconnect', True)
queue_complete = OptionStr('misc', 'queue_complete')

2
sabnzbd/dirscanner.py

@ -119,6 +119,7 @@ def ProcessArchiveFile(filename, path, pp=None, script=None, cat=None, catdir=No
nzo = None
if nzo:
nzo_ids.append(add_nzo(nzo))
nzo.update_rating()
zf.close()
try:
if not keep: os.remove(path)
@ -189,6 +190,7 @@ def ProcessSingleFile(filename, path, pp=None, script=None, cat=None, catdir=Non
if nzo:
nzo_ids.append(add_nzo(nzo))
nzo.update_rating()
try:
if not keep: os.remove(path)
except:

3
sabnzbd/downloader.py

@ -277,6 +277,9 @@ class Downloader(Thread):
return True
return False
def nzo_servers(self, nzo):
return filter(nzo.server_in_try_list, self.servers)
def maybe_block_server(self, server):
from sabnzbd.nzbqueue import NzbQueue
if server.optional and server.active and (server.bad_cons/server.threads) > 3:

38
sabnzbd/interface.py

@ -39,6 +39,7 @@ from sabnzbd.misc import real_path, to_units, \
from sabnzbd.panic import panic_old_queue
from sabnzbd.newswrapper import GetServerParms
from sabnzbd.newzbin import Bookmarks
from sabnzbd.rating import Rating
from sabnzbd.bpsmeter import BPSMeter
from sabnzbd.encoding import TRANS, xml_name, LatinFilter, unicoder, special_fixer, \
platform_encode, latin1, encode_for_xml
@ -878,7 +879,8 @@ class HistoryPage(object):
self.__verbose_list = []
self.__failed_only = False
self.__prim = prim
self.__edit_rating = None
@cherrypy.expose
def index(self, **kwargs):
if not check_access(): return Protected()
@ -894,6 +896,8 @@ class HistoryPage(object):
history['isverbose'] = self.__verbose
history['failed_only'] = failed_only
history['rating_enable'] = bool(cfg.rating_enable())
if cfg.newzbin_username() and cfg.newzbin_password():
history['newzbinDetails'] = True
@ -908,6 +912,12 @@ class HistoryPage(object):
history['lines'], history['fetched'], history['noofslots'] = build_history(limit=limit, start=start, verbose=self.__verbose, verbose_list=self.__verbose_list, search=search, failed_only=failed_only)
for line in history['lines']:
if self.__edit_rating is not None and line.get('nzo_id') == self.__edit_rating:
line['edit_rating'] = True
else:
line['edit_rating'] = ''
if search:
history['search'] = escape(search)
else:
@ -1026,6 +1036,29 @@ class HistoryPage(object):
del_hist_job(job, del_files=True)
raise dcRaiser(self.__root, kwargs)
@cherrypy.expose
def show_edit_rating(self, **kwargs):
msg = check_session(kwargs)
if msg: return msg
self.__edit_rating = kwargs.get('job');
raise queueRaiser(self.__root, kwargs)
@cherrypy.expose
def action_edit_rating(self, **kwargs):
flag_map = {'spam': Rating.FLAG_SPAM, 'encrypted': Rating.FLAG_ENCRYPTED, 'expired': Rating.FLAG_EXPIRED}
msg = check_session(kwargs)
if msg: return msg
try:
if kwargs.get('send'):
video = kwargs.get('video') if kwargs.get('video') != "-" else None
audio = kwargs.get('audio') if kwargs.get('audio') != "-" else None
flag = flag_map.get(kwargs.get('rating_flag'))
detail = kwargs.get('expired_host') if kwargs.get('expired_host') != '<Host>' else None
Rating.do.update_user_rating(kwargs.get('job'), video, audio, flag, detail)
except:
pass
self.__edit_rating = None;
raise queueRaiser(self.__root, kwargs)
#------------------------------------------------------------------------------
class ConfigPage(object):
@ -1176,7 +1209,8 @@ SWITCH_LIST = \
'ignore_samples', 'pause_on_post_processing', 'quick_check', 'nice', 'ionice',
'ssl_type', 'pre_script', 'pause_on_pwrar', 'ampm', 'sfv_check', 'folder_rename',
'unpack_check', 'quota_size', 'quota_day', 'quota_resume', 'quota_period',
'pre_check', 'max_art_tries', 'max_art_opt', 'fail_hopeless'
'pre_check', 'max_art_tries', 'max_art_opt', 'fail_hopeless',
'rating_enable', 'rating_api_key', 'rating_host', 'rating_feedback'
)
#------------------------------------------------------------------------------

220
sabnzbd/misc.py

@ -31,6 +31,7 @@ import socket
import time
import glob
import stat
import Queue
try:
socket.ssl
_HAVE_SSL = True
@ -1380,3 +1381,222 @@ def set_permissions(path, recursive=True):
set_chmod(path, umask, report)
else:
set_chmod(path, umask_file, report)
#------------------------------------------------------------------------------
# Backport of OrderedDict() class that runs on Python 2.4, 2.5, 2.6, 2.7 and pypy.
# Passes Python2.7's test suite and incorporates all the latest updates.
class OrderedDict(dict):
# An inherited dict maps keys to values.
# The inherited dict provides __getitem__, __len__, __contains__, and get.
# The remaining methods are order-aware.
# Big-O running times for all methods are the same as for regular dictionaries.
# The internal self.__map dictionary maps keys to links in a doubly linked list.
# The circular doubly linked list starts and ends with a sentinel element.
# The sentinel element never gets deleted (this simplifies the algorithm).
# Each link is stored as a list of length three: [PREV, NEXT, KEY].
def __init__(self, *args, **kwds):
'''Initialize an ordered dictionary. Signature is the same as for
regular dictionaries, but keyword arguments are not recommended
because their insertion order is arbitrary.
'''
if len(args) > 1:
raise TypeError('expected at most 1 arguments, got %d' % len(args))
try:
self.__root
except AttributeError:
self.__root = root = [] # sentinel node
root[:] = [root, root, None]
self.__map = {}
self.__update(*args, **kwds)
def __setitem__(self, key, value, dict_setitem=dict.__setitem__):
# Setting a new item creates a new link which goes at the end of the linked
# list, and the inherited dictionary is updated with the new key/value pair.
if key not in self:
root = self.__root
last = root[0]
last[1] = root[0] = self.__map[key] = [last, root, key]
dict_setitem(self, key, value)
def __delitem__(self, key, dict_delitem=dict.__delitem__):
# Deleting an existing item uses self.__map to find the link which is
# then removed by updating the links in the predecessor and successor nodes.
dict_delitem(self, key)
link_prev, link_next, key = self.__map.pop(key)
link_prev[1] = link_next
link_next[0] = link_prev
def __iter__(self):
root = self.__root
curr = root[1]
while curr is not root:
yield curr[2]
curr = curr[1]
def __reversed__(self):
root = self.__root
curr = root[0]
while curr is not root:
yield curr[2]
curr = curr[0]
def clear(self):
try:
for node in self.__map.itervalues():
del node[:]
root = self.__root
root[:] = [root, root, None]
self.__map.clear()
except AttributeError:
pass
dict.clear(self)
def popitem(self, last=True):
'''od.popitem() -> (k, v), return and remove a (key, value) pair.
Pairs are returned in LIFO order if last is true or FIFO order if false.
'''
if not self:
raise KeyError('dictionary is empty')
root = self.__root
if last:
link = root[0]
link_prev = link[0]
link_prev[1] = root
root[0] = link_prev
else:
link = root[1]
link_next = link[1]
root[1] = link_next
link_next[0] = root
key = link[2]
del self.__map[key]
value = dict.pop(self, key)
return key, value
# -- the following methods do not depend on the internal structure --
def keys(self):
return list(self)
def values(self):
return [self[key] for key in self]
def items(self):
return [(key, self[key]) for key in self]
def iterkeys(self):
return iter(self)
def itervalues(self):
for k in self:
yield self[k]
def iteritems(self):
for k in self:
yield (k, self[k])
def update(*args, **kwds):
if len(args) > 2:
raise TypeError('update() takes at most 2 positional '
'arguments (%d given)' % (len(args),))
elif not args:
raise TypeError('update() takes at least 1 argument (0 given)')
self = args[0]
# Make progressively weaker assumptions about "other"
other = ()
if len(args) == 2:
other = args[1]
if isinstance(other, dict):
for key in other:
self[key] = other[key]
elif hasattr(other, 'keys'):
for key in other.keys():
self[key] = other[key]
else:
for key, value in other:
self[key] = value
for key, value in kwds.items():
self[key] = value
__update = update # let subclasses override update without breaking __init__
__marker = object()
def pop(self, key, default=__marker):
if key in self:
result = self[key]
del self[key]
return result
if default is self.__marker:
raise KeyError(key)
return default
def setdefault(self, key, default=None):
if key in self:
return self[key]
self[key] = default
return default
def __repr__(self, _repr_running={}):
call_key = id(self), _get_ident()
if call_key in _repr_running:
return '...'
_repr_running[call_key] = 1
try:
if not self:
return '%s()' % (self.__class__.__name__,)
return '%s(%r)' % (self.__class__.__name__, self.items())
finally:
del _repr_running[call_key]
def __reduce__(self):
items = [[k, self[k]] for k in self]
inst_dict = vars(self).copy()
for k in vars(OrderedDict()):
inst_dict.pop(k, None)
if inst_dict:
return (self.__class__, (items,), inst_dict)
return self.__class__, (items,)
def copy(self):
return self.__class__(self)
@classmethod
def fromkeys(cls, iterable, value=None):
d = cls()
for key in iterable:
d[key] = value
return d
def __eq__(self, other):
if isinstance(other, OrderedDict):
return len(self)==len(other) and self.items() == other.items()
return dict.__eq__(self, other)
def __ne__(self, other):
return not self == other
# -- the following methods are only used in Python 2.7 --
def viewkeys(self):
return KeysView(self)
def viewvalues(self):
return ValuesView(self)
def viewitems(self):
return ItemsView(self)
#------------------------------------------------------------------------------
# A queue which ignores duplicates but maintains ordering
class OrderedSetQueue(Queue.Queue):
def _init(self, maxsize):
self.queue = OrderedDict()
def _put(self, item):
self.queue[item] = None
def _get(self):
return self.queue.popitem()[0]

16
sabnzbd/nzbstuff.py

@ -45,6 +45,7 @@ from sabnzbd.misc import to_units, cat_to_opts, cat_convert, sanitize_foldername
import sabnzbd.cfg as cfg
from sabnzbd.trylist import TryList
from sabnzbd.encoding import unicoder, platform_encode, latin1, name_fixer
from sabnzbd.rating import Rating
__all__ = ['Article', 'NzbFile', 'NzbObject']
@ -750,7 +751,7 @@ class NzbObject(TryList):
cat = cat_convert(grp)
if cat:
break
if cfg.create_group_folders():
self.dirprefix.append(self.group)
@ -1262,6 +1263,19 @@ class NzbObject(TryList):
self.files[pos+1] = nzf
self.files[pos] = tmp_nzf
# Determine if rating information (including site identifier so rating can be updated)
# is present in metadata and if so store it
def update_rating(self):
try:
def _get_first_meta(type):
values = self.meta.get('x-rating-' + type, None)
return values[0] if values else None
rating_types = ['id', 'video', 'videocnt', 'audio', 'audiocnt', 'voteup' ,'votedown']
rs = map(_get_first_meta, rating_types)
Rating.do.add_rating(rs[0], self.nzo_id, rs[1], rs[2], rs[3], rs[4], rs[5], rs[6])
except:
pass
## end nzo.Mutators #######################################################
###########################################################################
@property

10
sabnzbd/postproc.py

@ -39,6 +39,7 @@ from sabnzbd.constants import REPAIR_PRIORITY, TOP_PRIORITY, POSTPROC_QUEUE_FILE
POSTPROC_QUEUE_VERSION, sample_match, JOB_ADMIN, Status, VERIFIED_FILE
from sabnzbd.encoding import TRANS, unicoder
from sabnzbd.newzbin import Bookmarks
from sabnzbd.rating import Rating
import sabnzbd.emailer as emailer
import sabnzbd.dirscanner as dirscanner
import sabnzbd.downloader
@ -479,6 +480,15 @@ def process_job(nzo):
## Force error for empty result
all_ok = all_ok and not empty
## Update indexer with results
if nzo.encrypted > 0:
Rating.do.update_auto_flag(nzo.nzo_id, Rating.FLAG_ENCRYTPTED)
if empty:
hosts = map(lambda s: s.host, sabnzbd.downloader.Downloader.do.nzo_servers(nzo))
if not hosts: hosts = [None]
for host in hosts:
Rating.do.update_auto_flag(nzo.nzo_id, Rating.FLAG_EXPIRED, host)
## Show final status in history
if all_ok:
growler.send_notification(T('Download Completed'), filename, 'complete')

261
sabnzbd/rating.py

@ -0,0 +1,261 @@
#!/usr/bin/python -OO
# Copyright 2008-2012 The SABnzbd-Team <team@sabnzbd.org>
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
"""
sabnzbd.rating - Rating support functions
"""
import httplib
import urllib
import time
import logging
import copy
import socket
import random
try:
socket.ssl
_HAVE_SSL = True
except:
_HAVE_SSL = False
from threading import *
import sabnzbd
from sabnzbd.decorators import synchronized
from sabnzbd.misc import OrderedSetQueue
import sabnzbd.cfg as cfg
RATING_URL = "/releaseRatings/releaseRatings.php"
RATING_LOCK = RLock()
_g_warnings = 0
def _warn(msg):
global _g_warnings
_g_warnings += 1
if _g_warnings < 3:
logging.warning(msg)
def _reset_warn():
global _g_warnings
_g_warnings = 0
class NzbRating(object):
def __init__(self):
self.avg_video = 0
self.avg_video_cnt = 0
self.avg_audio = 0
self.avg_audio_cnt = 0
self.avg_vote_up = 0
self.avg_vote_down = 0
self.user_video = None
self.user_audio = None
self.user_vote = None
self.user_flag = {}
self.auto_flag = {}
self.changed = 0
class Rating(Thread):
VERSION = 1
VOTE_UP = 1
VOTE_DOWN = 2
FLAG_OK = 0
FLAG_SPAM = 1
FLAG_ENCRYPTED = 2
FLAG_EXPIRED = 3
FLAG_OTHER = 4
FLAG_COMMENT = 5
CHANGED_USER_VIDEO = 0x01
CHANGED_USER_AUDIO = 0x02
CHANGED_USER_VOTE = 0x04
CHANGED_USER_FLAG = 0x08
CHANGED_AUTO_FLAG = 0x10
do = None
def __init__(self):
Rating.do = self
self.shutdown = False
self.queue = OrderedSetQueue()
try:
(self.version, self.ratings, self.nzo_indexer_map) = sabnzbd.load_admin("Rating.sab")
if (self.version != Rating.VERSION):
raise Exception()
except:
self.version = Rating.VERSION
self.ratings = {}
self.nzo_indexer_map = {}
Thread.__init__(self)
if not _HAVE_SSL:
logging.warning('Ratings server requires secure connection')
self.stop()
def stop(self):
self.shutdown = True
self.queue.put(None) # Unblock queue
def run(self):
self.shutdown = False
while not self.shutdown:
time.sleep(0.5)
indexer_id = self.queue.get()
try:
if indexer_id and not self._send_rating(indexer_id):
for i in range(0, 60):
if self.shutdown: break
time.sleep(1)
self.queue.put(indexer_id)
except:
pass
logging.debug('Stopping ratings')
@synchronized(RATING_LOCK)
def save(self):
if self.ratings and self.nzo_indexer_map:
sabnzbd.save_admin((self.version, self.ratings, self.nzo_indexer_map), "Rating.sab")
# The same file may be uploaded multiple times creating a new nzo_id each time
@synchronized(RATING_LOCK)
def add_rating(self, indexer_id, nzo_id, video, video_cnt, audio, audio_cnt, vote_up, vote_down):
if indexer_id and nzo_id and (video or audio or vote_up or vote_down):
logging.debug('Add rating (%s, %s: %s, %s, %s, %s)', indexer_id, nzo_id, video, audio, vote_up, vote_down)
try:
rating = self.ratings.get(indexer_id, NzbRating())
if video and video_cnt:
rating.avg_video = int(float(video))
rating.avg_video_cnt = int(float(video_cnt))
if audio and audio_cnt:
rating.avg_audio = int(float(audio))
rating.avg_audio_cnt = int(float(audio_cnt))
if vote_up: rating.avg_vote_up = int(float(vote_up))
if vote_down: rating.avg_vote_down = int(float(vote_down))
self.ratings[indexer_id] = rating
self.nzo_indexer_map[nzo_id] = indexer_id
except:
pass
@synchronized(RATING_LOCK)
def update_user_rating(self, nzo_id, video, audio, vote, flag, flag_detail = None):
logging.debug('Updating user rating (%s: %s, %s, %s, %s)', nzo_id, video, audio, vote, flag)
if nzo_id not in self.nzo_indexer_map:
logging.warning('indexer id (%s) not found for ratings file', nzo_id)
return
indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id]
if video:
rating.user_video = int(video)
rating.avg_video = int((rating.avg_video_cnt * rating.avg_video + rating.user_video) / (rating.avg_video_cnt + 1))
rating.changed = rating.changed | Rating.CHANGED_USER_VIDEO
if audio:
rating.user_audio = int(audio)
rating.avg_audio = int((rating.avg_audio_cnt * rating.avg_audio + rating.user_audio) / (rating.avg_audio_cnt + 1))
rating.changed = rating.changed | Rating.CHANGED_USER_AUDIO
if flag:
rating.user_flag = { 'val': int(flag), 'detail': flag_detail }
rating.changed = rating.changed | Rating.CHANGED_USER_FLAG
if vote and not rating.user_vote:
rating.user_vote = int(vote)
rating.changed = rating.changed | Rating.CHANGED_USER_VOTE
if rating.user_vote == Rating.VOTE_UP:
rating.avg_vote_up += 1
else:
rating.avg_vote_down += 1
self.queue.put(indexer_id)
@synchronized(RATING_LOCK)
def update_auto_flag(self, nzo_id, flag, flag_detail = None):
if not flag or not cfg.rating_feeback():
return
logging.debug('Updating auto flag (%s: %s)', nzo_id, flag)
if nzo_id not in self.nzo_indexer_map:
logging.warning('indexer id (%s) not found for ratings file', nzo_id)
return
indexer_id = self.nzo_indexer_map[nzo_id]
rating = self.ratings[indexer_id]
rating.auto_flag = { 'val': int(flag), 'detail': flag_detail }
rating.changed = rating.changed | Rating.CHANGED_AUTO_FLAG
self.queue.put(indexer_id)
@synchronized(RATING_LOCK)
def get_rating_by_nzo(self, nzo_id):
if nzo_id not in self.nzo_indexer_map:
return None
return copy.copy(self.ratings[self.nzo_indexer_map[nzo_id]])
@synchronized(RATING_LOCK)
def _get_rating_by_indexer(self, indexer_id):
return copy.copy(self.ratings[indexer_id])
def _flag_request(self, val, flag_detail, auto):
if val == Rating.FLAG_SPAM:
return {'m': 'rs', 'auto': auto}
if val == Rating.FLAG_ENCRYPTED:
return {'m': 'rp', 'auto': auto}
if val == Rating.FLAG_EXPIRED:
expired_host = flag_detail if flag_detail and len(flag_detail) > 0 else 'Other'
return {'m': 'rpr', 'pr': expired_host, 'auto': auto};
if (val == Rating.FLAG_OTHER) and flag_detail and len(flag_detail) > 0:
return {'m': 'o', 'r': flag_detail};
if (val == Rating.FLAG_COMMENT) and flag_detail and len(flag_detail) > 0:
return {'m': 'rc', 'r': flag_detail};
def _send_rating(self, indexer_id):
logging.debug('Updating indexer rating (%s)', indexer_id)
api_key = cfg.rating_api_key()
rating_host = cfg.rating_host()
if not api_key or not rating_host:
return False
requests = []
_headers = {'User-agent' : 'SABnzbd+/%s' % sabnzbd.version.__version__, 'Content-type': 'application/x-www-form-urlencoded'}
rating = self._get_rating_by_indexer(indexer_id) # Requesting info here ensures always have latest information even on retry
if rating.changed & Rating.CHANGED_USER_VIDEO:
requests.append({'m': 'r', 'r': 'videoQuality', 'rn': rating.user_video})
if rating.changed & Rating.CHANGED_USER_AUDIO:
requests.append({'m': 'r', 'r': 'audioQuality', 'rn': rating.user_audio})
if rating.changed & Rating.CHANGED_USER_VOTE:
up_down = 'up' if rating.user_vote == Rating.VOTE_UP else 'down'
requests.append({'m': 'v', 'v': up_down, 'r': 'overall'})
if rating.changed & Rating.CHANGED_USER_FLAG:
requests.append(self._flag_request(rating.user_flag.get('val'), rating.user_flag.get('detail'), 0))
if rating.changed & Rating.CHANGED_AUTO_FLAG:
requests.append(self._flag_request(rating.auto_flag.get('val'), rating.auto_flag.get('detail'), 1))
try:
conn = httplib.HTTPSConnection(rating_host)
for request in filter(lambda r: r is not None, requests):
request['apikey'] = api_key
request['i'] = indexer_id
conn.request('POST', RATING_URL, urllib.urlencode(request), headers = _headers)
response = conn.getresponse()
response.read()
if response.status == httplib.UNAUTHORIZED:
_warn('Ratings server unauthorized user')
return False
elif response.status != httplib.OK:
_warn('Ratings server failed to process request (%s, %s)' % response.status, response.reason)
return False
rating.changed = 0
_reset_warn()
return True
except:
_warn('Problem accessing ratings server: %s' % rating_host)
return False

26
sabnzbd/skintext.py

@ -101,6 +101,14 @@ SKIN_TEXT = {
'homePage' : TT('Home page'), #: Home page of the SABnzbd project
'source' : TT('Source'), #: Where to find the SABnzbd sourcecode
'or' : TT('or'), #: Used in "IRC or IRC-Webaccess"
'host' : TT('Host'),
'comment' : TT('Comment'),
'send' : TT('Send'),
'cancel' : TT('Cancel'),
'other' : TT('Other'),
'report' : TT('Report'),
'video' : TT('Video'),
'audio' : TT('Audio'),
# General template elements
'signOn' : TT('The automatic usenet download tool'), #: SABnzbd's theme line
@ -232,8 +240,11 @@ SKIN_TEXT = {
'purgeCompl' : TT('Purge Completed NZBs'), #: Button to delete all completed jobs in History
'opt-extra-NZB' : TT('Optional Supplemental NZB'), #: Button to add NZB to failed job in History
'msg-path' : TT('Path'), #: Path as displayed in History details
'spam' : TT('Virus/spam'),
'encrypted' : TT('Passworded'),
'expired' : TT('Out of retention'),
'otherProblem' : TT('Other problem'),
# Connections page
'link-forceDisc' : TT('Force Disconnect'), #: Status page button
'askTestEmail' : TT('This will send a test email to your account.'),
@ -271,6 +282,7 @@ SKIN_TEXT = {
'version' : TT('Version'),
'uptime' : TT('Uptime'),
'backup' : TT('Backup'), #: Indicates that server is Backup server in Status page
'oznzb' : TT('OZnzb'),
# Config->General
'generalConfig' : TT('General configuration'),
@ -445,6 +457,7 @@ SKIN_TEXT = {
'swtag-pp' : TT('Post processing'),
'swtag-naming' : TT('Naming'),
'swtag-quota' : TT('Quota'),
'swtag-indexing' : TT('Indexing'),
'opt-quota_size' : TT('Size'), #: Size of the download quota
'explain-quota_size' : TT('How much can be downloaded this month (K/M/G)'),
'opt-quota_day' : TT('Reset day'), #: Reset day of the download quota
@ -461,7 +474,13 @@ SKIN_TEXT = {
'explain-max_art_opt' : TT('Apply maximum retries only to optional servers'),
'opt-fail_hopeless' : TT('Abort jobs that cannot be completed'),
'explain-fail_hopeless' : TT('When during download it becomes clear that too much data is missing, abort the job'),
'opt-rating_enable' : TT('Enable OZnzb Integration'),
'explain-rating_enable' : TT('Enhanced functionality including ratings and extra status information is available when connected to OZnzb indexer.'),
'opt-rating_api_key' : TT('Site API Key'),
'explain-rating_api_key' : TT('This key provides identity to indexer. Refer to https://www.oznzb.com/profile.'),
'tip-rating_api_key' : TT('Refer to https://www.oznzb.com/profile'),
'opt-rating_feedback' : TT('Automatic Feedback'),
'explain-rating_feedback' : TT('Send automatically calculated validation results for downloads to indexer.'),
# Config->Server
'configServer' : TT('Server configuration'), #: Caption
@ -871,6 +890,7 @@ SKIN_TEXT = {
'wizard-port-eg' : TT('E.g. 119 or 563 for SSL'), #: Wizard port number examples
'wizard-exit' : TT('Exit SABnzbd'), #: Wizard EXIT button on first page
'wizard-start' : TT('Start Wizard'), #: Wizard START button on first page
'wizard-create-account' : TT('If you do not have an account it can be created at '),
#Special
'yourRights' : TT('''

29
sabnzbd/wizard.py

@ -41,7 +41,7 @@ class Wizard(object):
self.__web_dir = sabnzbd.WIZARD_DIR
self.__prim = prim
self.info = {'webdir': sabnzbd.WIZARD_DIR,
'steps':3, 'version':sabnzbd.__version__,
'steps':4, 'version':sabnzbd.__version__,
'T': T}
@cherrypy.expose
@ -151,7 +151,7 @@ class Wizard(object):
@cherrypy.expose
def three(self, **kwargs):
""" Accept webserver parms and show Indexers page """
""" Accept webserver parms and show Indexer page """
if kwargs:
if 'access' in kwargs:
cfg.cherryhost.set(kwargs['access'])
@ -161,20 +161,39 @@ class Wizard(object):
cfg.password.set(kwargs.get('web_pass', ''))
if not cfg.username() or not cfg.password():
sabnzbd.interface.set_auth(cherrypy.config)
config.save_config()
# Create indexer page
info = self.info.copy()
info['num'] = '&raquo; %s' % T('Step Three')
info['number'] = 3
info['T'] = Ttemplate
info['rating_enable'] = cfg.rating_enable()
info['rating_api_key'] = cfg.rating_api_key()
template = Template(file=os.path.join(self.__web_dir, 'three.html'),
searchList=[info], compilerSettings=sabnzbd.interface.DIRECTIVES)
return template.respond()
@cherrypy.expose
def four(self, **kwargs):
if kwargs:
cfg.rating_enable.set(kwargs.get('rating_enable', 0))
cfg.rating_api_key.set(kwargs.get('rating_api_key', ''))
config.save_config()
# Show Restart screen
info = self.info.copy()
info['num'] = '&raquo; %s' % T('Step Three')
info['number'] = 3
info['num'] = '&raquo; %s' % T('Step Four')
info['number'] = 4
info['helpuri'] = 'http://wiki.sabnzbd.org/'
info['session'] = cfg.api_key()
info['access_url'], info['urls'] = self.get_access_info()
info['T'] = Ttemplate
template = Template(file=os.path.join(self.__web_dir, 'three.html'),
template = Template(file=os.path.join(self.__web_dir, 'four.html'),
searchList=[info], compilerSettings=sabnzbd.interface.DIRECTIVES)
return template.respond()

Loading…
Cancel
Save