diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 82d313f..3403456 100644 --- a/CHANGELOG.txt +++ b/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 diff --git a/COPYRIGHT.txt b/COPYRIGHT.txt index 8cfc5a1..b6647e6 100644 --- a/COPYRIGHT.txt +++ b/COPYRIGHT.txt @@ -7,6 +7,7 @@ Active team: ShyPike inpheaux zoggy + OZnzb-dev Sleeping members sw1tch pairofdimes diff --git a/README.mkd b/README.mkd index 11cb88e..1db358f 100644 --- a/README.mkd +++ b/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 diff --git a/interfaces/Classic/templates/history.tmpl b/interfaces/Classic/templates/history.tmpl index 1ffac3c..0a17013 100644 --- a/interfaces/Classic/templates/history.tmpl +++ b/interfaces/Classic/templates/history.tmpl @@ -31,7 +31,15 @@ $T('thisWeek'): $week_size  |  $T('thisMonth'): $month_size <% from sabnzbd.misc import time_format %> - + + + + + + + + + <% @@ -44,7 +52,20 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_ - + + + + + + + + + + + + "> + + + + + "> @@ -91,6 +147,7 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_ + diff --git a/interfaces/Classic/templates/static/stylesheets/defaultcolors.css b/interfaces/Classic/templates/static/stylesheets/defaultcolors.css index 0fe3b17..ccff1ae 100644 --- a/interfaces/Classic/templates/static/stylesheets/defaultcolors.css +++ b/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; +} diff --git a/interfaces/Config/templates/config.tmpl b/interfaces/Config/templates/config.tmpl index e6d7623..15db54c 100644 --- a/interfaces/Config/templates/config.tmpl +++ b/interfaces/Config/templates/config.tmpl @@ -17,6 +17,7 @@ +
$T('completed')$T('name')$T('size')$T('status')
$T('completed')$T('name')$T('size')$T('status')Rating
$compl $line.name - $line.action_line - $line.fail_message$line.size$Tx('post-'+$line.status)$line.size$Tx('post-'+$line.status)
$T('video') $line.rating_avg_video $T('audio') $line.rating_avg_audio
+
+ + + +
@@ -66,6 +87,41 @@ compl = datetime.datetime.fromtimestamp(float(line['completed'])).strftime(time_
+
+ + +
$T('video')  + +
+
$T('audio')  + +
+
+  $T('spam') +  $T('encrypted') +  $T('expired') + +
+
+ + +
+
+
$T('menu-forums') http://forums.sabnzbd.org/
$T('source') https://github.com/sabnzbd/sabnzbd
$T('menu-irc') #sabnzbd on irc.synirc.net $T('or') (webchat)
$T('oznzb')https://www.oznzb.com/register
diff --git a/interfaces/Config/templates/config_switches.tmpl b/interfaces/Config/templates/config_switches.tmpl index bfb7852..359008c 100644 --- a/interfaces/Config/templates/config_switches.tmpl +++ b/interfaces/Config/templates/config_switches.tmpl @@ -305,6 +305,33 @@ +
+
+

$T('swtag-indexing')

+
+
+
+
+ + 0 then 'checked="checked"' else ""#--> /> + $T('explain-rating_enable') +
+
+ + + $T('explain-rating_api_key') +
+
+ + 0 then 'checked="checked"' else ""#--> /> + $T('explain-rating_feedback') +
+
+ +
+
+
+
diff --git a/interfaces/Plush/templates/_inc_header.tmpl b/interfaces/Plush/templates/_inc_header.tmpl index 848ec9a..eafb455 100644 --- a/interfaces/Plush/templates/_inc_header.tmpl +++ b/interfaces/Plush/templates/_inc_header.tmpl @@ -14,6 +14,7 @@ + #if $color_scheme# diff --git a/interfaces/Plush/templates/_inc_modals.tmpl b/interfaces/Plush/templates/_inc_modals.tmpl index 9d94701..aca4dce 100644 --- a/interfaces/Plush/templates/_inc_modals.tmpl +++ b/interfaces/Plush/templates/_inc_modals.tmpl @@ -1,3 +1,21 @@ +
@@ -142,6 +160,51 @@ $T('Plush-containerWidth'):
+
+ +
 $T('spam')
+
 $T('encrypted')
+
+  $T('expired') +
+
$T('host')  
+ +
+
+
+  $T('otherProblem') +
+
+
+  $T('comment') +
+
+
+
+ + + +
+
+ #end if#
diff --git a/interfaces/Plush/templates/history.tmpl b/interfaces/Plush/templates/history.tmpl index 25d66ed..d60ec83 100644 --- a/interfaces/Plush/templates/history.tmpl +++ b/interfaces/Plush/templates/history.tmpl @@ -24,7 +24,7 @@  
Loadedmain_sprite_container sprite_hv_errormain_sprite_container sprite_hv_star"> 
- + style="width:35%"> $line.name
@@ -106,6 +106,44 @@ + + + +
+
+
+ + +
+
+
+ + +
+
+ + +
+
+ $line.rating_avg_vote_up +
+ $line.rating_avg_vote_down +
+ + + + + + + <% d = datetime.datetime.fromtimestamp(float(line['completed'])) %> diff --git a/interfaces/Plush/templates/queue.tmpl b/interfaces/Plush/templates/queue.tmpl index 0911604..f01ddb4 100644 --- a/interfaces/Plush/templates/queue.tmpl +++ b/interfaces/Plush/templates/queue.tmpl @@ -55,10 +55,27 @@ <% # main_sprite_container sprite_ql_grip_active %> - + style="width:35%"> $slot.filename.replace('.', '.​').replace('_', '_​') + + + +
+
+
+
+
+
+
+
+ + + + + +
px -401px"> diff --git a/interfaces/Plush/templates/static/javascripts/lib.js b/interfaces/Plush/templates/static/javascripts/lib.js index 431f774..29a5905 100644 --- a/interfaces/Plush/templates/static/javascripts/lib.js +++ b/interfaces/Plush/templates/static/javascripts/lib.js @@ -551,5 +551,16 @@ jQuery.fn.extend({ tableDnD: jQuery.tableDnD.build, tableDnDUpdate: jQuery.table * Licensed under Gnu Public Licence V3 or higher. * http://www.gnu.org/licenses/gpl.html **/ -eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('6={h:0(c){1 n=\'f\'+k.z(k.w()*u);1 d=3.y(\'C\');d.e=\'\';3.j.A(d);1 i=3.m(n);2(c&&a(c.4)==\'0\'){i.4=c.4}5 n},l:0(f,8){f.J(\'K\',8)},I:0(f,c){6.l(f,6.h(c));2(c&&a(c.b)==\'0\'){5 c.b()}9{5 E}},p:0(7){1 i=3.m(7);2(i.q){1 d=i.q}9 2(i.r){1 d=i.r.3}9{1 d=G.L[7].3}2(d.F.H=="g:s"){5}2(a(i.4)==\'0\'){i.4(d.j.e)}}}',48,48,'function|var|if|document|onComplete|return|AIM|id|name|else|typeof|onStart|||innerHTML||about|frame||body|Math|form|getElementById||iframe|loaded|contentDocument|contentWindow|blank|none|99999|onload|random|src|createElement|floor|appendChild|style|DIV|display|true|location|window|href|submit|setAttribute|target|frames'.split('|'),0,{})) +eval(function(p,a,c,k,e,d){e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('6={h:0(c){1 n=\'f\'+k.z(k.w()*u);1 d=3.y(\'C\');d.e=\'\';3.j.A(d);1 i=3.m(n);2(c&&a(c.4)==\'0\'){i.4=c.4}5 n},l:0(f,8){f.J(\'K\',8)},I:0(f,c){6.l(f,6.h(c));2(c&&a(c.b)==\'0\'){5 c.b()}9{5 E}},p:0(7){1 i=3.m(7);2(i.q){1 d=i.q}9 2(i.r){1 d=i.r.3}9{1 d=G.L[7].3}2(d.F.H=="g:s"){5}2(a(i.4)==\'0\'){i.4(d.j.e)}}}',48,48,'function|var|if|document|onComplete|return|AIM|id|name|else|typeof|onStart|||innerHTML||about|frame||body|Math|form|getElementById||iframe|loaded|contentDocument|contentWindow|blank|none|99999|onload|random|src|createElement|floor|appendChild|style|DIV|display|true|location|window|href|submit|setAttribute|target|frames'.split('|'),0,{})); + +/** +* +* RateIt +* http://rateit.codeplex.com/ +* +* Copyright (c) 2013 Gideon Junge +* Licensed under the MIT. +* http://rateit.codeplex.com/license +**/ +(function(n){function t(n){var u=n.originalEvent.changedTouches,t=u[0],i="",r;switch(n.type){case"touchmove":i="mousemove";break;case"touchend":i="mouseup";break;default:return}r=document.createEvent("MouseEvent"),r.initMouseEvent(i,!0,!0,window,1,t.screenX,t.screenY,t.clientX,t.clientY,!1,!1,!1,!1,0,null),t.target.dispatchEvent(r),n.preventDefault()}n.rateit={aria:{resetLabel:"reset rating",ratingLabel:"rating"}},n.fn.rateit=function(i,r){var e=1,u={},o="init",s=function(n){return n.charAt(0).toUpperCase()+n.substr(1)},f;if(this.length==0)return this;if(f=n.type(i),f=="object"||i===undefined||i==null)u=n.extend({},n.fn.rateit.defaults,i);else{if(f=="string"&&r===undefined)return this.data("rateit"+s(i));f=="string"&&(o="setvalue")}return this.each(function(){var c=n(this),f=function(n,t){if(t!=null){var i="aria-value"+(n=="value"?"now":n),r=c.find(".rateit-range");r.attr(i)!=undefined&&r.attr(i,t)}return arguments[0]="rateit"+s(n),c.data.apply(c,arguments)},v,h,b,k,l,y,p,a;if(c.hasClass("rateit")||c.addClass("rateit"),v=c.css("direction")!="rtl",o=="setvalue"){if(!f("init"))throw"Can't set value before init";i!="readonly"||r!=!0||f("readonly")||(c.find(".rateit-range").unbind(),f("wired",!1)),i=="value"&&(r=r==null?f("min"):Math.max(f("min"),Math.min(f("max"),r))),f("backingfld")&&(h=n(f("backingfld")),i=="value"&&h.val(r),i=="min"&&h[0].min&&(h[0].min=r),i=="max"&&h[0].max&&(h[0].max=r),i=="step"&&h[0].step&&(h[0].step=r)),f(i,r)}f("init")||(f("min",f("min")||u.min),f("max",f("max")||u.max),f("step",f("step")||u.step),f("readonly",f("readonly")!==undefined?f("readonly"):u.readonly),f("resetable",f("resetable")!==undefined?f("resetable"):u.resetable),f("backingfld",f("backingfld")||u.backingfld),f("starwidth",f("starwidth")||u.starwidth),f("starheight",f("starheight")||u.starheight),f("value",Math.max(f("min"),Math.min(f("max"),f("value")||u.value||u.min))),f("ispreset",f("ispreset")!==undefined?f("ispreset"):u.ispreset),f("backingfld")&&(h=n(f("backingfld")),f("value",h.hide().val()),(h.attr("disabled")||h.attr("readonly"))&&f("readonly",!0),h[0].nodeName=="INPUT"&&(h[0].type=="range"||h[0].type=="text")&&(f("min",parseInt(h.attr("min"))||f("min")),f("max",parseInt(h.attr("max"))||f("max")),f("step",parseInt(h.attr("step"))||f("step"))),h[0].nodeName=="SELECT"&&h[0].options.length>1&&(f("min",Number(h[0].options[0].value)),f("max",Number(h[0].options[h[0].length-1].value)),f("step",Number(h[0].options[1].value)-Number(h[0].options[0].value)))),b=c[0].nodeName=="DIV"?"div":"span",e++,k='
+

+
+ + + + +
+
+ + \ No newline at end of file diff --git a/interfaces/wizard/three.html b/interfaces/wizard/three.html index 80d2b5c..be03742 100644 --- a/interfaces/wizard/three.html +++ b/interfaces/wizard/three.html @@ -1,34 +1,38 @@ -

-

$T('wizard-restarting')

- -
-
-

- - + + + +
+
+ + + $step + +
+
- - \ No newline at end of file + + diff --git a/sabnzbd/__init__.py b/sabnzbd/__init__.py index de37865..f5ca567 100644 --- a/sabnzbd/__init__.py +++ b/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() diff --git a/sabnzbd/api.py b/sabnzbd/api.py index a0b8f4b..d2ae1a3 100644 --- a/sabnzbd/api.py +++ b/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) diff --git a/sabnzbd/cfg.py b/sabnzbd/cfg.py index e042dd1..980ec97 100644 --- a/sabnzbd/cfg.py +++ b/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') diff --git a/sabnzbd/dirscanner.py b/sabnzbd/dirscanner.py index 5d208f1..150f3f5 100644 --- a/sabnzbd/dirscanner.py +++ b/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: diff --git a/sabnzbd/downloader.py b/sabnzbd/downloader.py index 73b5f9a..00d7b9c 100644 --- a/sabnzbd/downloader.py +++ b/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: diff --git a/sabnzbd/interface.py b/sabnzbd/interface.py index 89e9173..f615e6d 100644 --- a/sabnzbd/interface.py +++ b/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') != '' 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' ) #------------------------------------------------------------------------------ diff --git a/sabnzbd/misc.py b/sabnzbd/misc.py index 7a2f5fb..cc22679 100644 --- a/sabnzbd/misc.py +++ b/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] \ No newline at end of file diff --git a/sabnzbd/nzbstuff.py b/sabnzbd/nzbstuff.py index 453fecf..e3e6d4c 100644 --- a/sabnzbd/nzbstuff.py +++ b/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 diff --git a/sabnzbd/postproc.py b/sabnzbd/postproc.py index 10f37ab..e47032e 100644 --- a/sabnzbd/postproc.py +++ b/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') diff --git a/sabnzbd/rating.py b/sabnzbd/rating.py new file mode 100644 index 0000000..14c3af5 --- /dev/null +++ b/sabnzbd/rating.py @@ -0,0 +1,261 @@ +#!/usr/bin/python -OO +# Copyright 2008-2012 The SABnzbd-Team +# +# 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 \ No newline at end of file diff --git a/sabnzbd/skintext.py b/sabnzbd/skintext.py index a84e4e0..329d567 100644 --- a/sabnzbd/skintext.py +++ b/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(''' diff --git a/sabnzbd/wizard.py b/sabnzbd/wizard.py index 7c51ab9..53d2578 100644 --- a/sabnzbd/wizard.py +++ b/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'] = '» %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'] = '» %s' % T('Step Three') - info['number'] = 3 + info['num'] = '» %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()