diff --git a/CHANGES.md b/CHANGES.md index e77ccf1..5f1fb53 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,25 @@ ### 0.21.0 (2019-xx-xx xx:xx:xx UTC) +* Add ability to use multiple SG apikeys +* Add UI for multiple apikeys to config/General/Web Interface +* Add jquery-qrcode 0.17.0 +* Change add apikey name to ERROR log messages +* Change add logging of errors from api +* Change add remote ip to error message +* Change add print command name for api in debug log +* Change add warning message to log if old Sick-Beard api call is used +* Change add an api call mapping helper for name changed functions (for printed warnings) +* Change ui typo in apiBuilder +* Fix display of fanart in apibuilder +* Add help command to apiBuilder and fix help call +* Fix add shows via api +* Change fix sg.searchqueue output +* Add missing sg.show.delete parameter "full" +* Add missing sg.setdefaults and sg.shutdown methods +* Change increase api version because missing sg.* methods are added +* Change add some extra checks for Sick-Beard call add (existing) show +* Change patch imdbpie to add cachedir folder and set imdbpie cachedir in SG +* Fix force search return values * Update attr 19.2.0.dev0 (154b4e5) to 19.2.0.dev0 (daf2bc8) * Update Beautiful Soup 4.7.1 (r497) to 4.8.0 (r526) * Update bencode to 2.1.0 (e8290df) diff --git a/HACKS.txt b/HACKS.txt index 94db0ec..cf446d2 100644 --- a/HACKS.txt +++ b/HACKS.txt @@ -9,6 +9,7 @@ Libs with customisations... /lib/hachoir_metadata/riff.py /lib/hachoir_parser/guess.py /lib/hachoir_parser/misc/torrent.py +/lib/imdbpie /lib/lockfile/mkdirlockfile.py /lib/rtorrent /lib/scandir/scandir.py diff --git a/gui/slick/css/dark.css b/gui/slick/css/dark.css index f2c5c7f..0f57aa5 100644 --- a/gui/slick/css/dark.css +++ b/gui/slick/css/dark.css @@ -651,10 +651,12 @@ config*.tmpl border-bottom:1px dotted #666 } +.qr-btn .glyphicon-qrcode, .component-group-desc p{ color:#ddd } +.dotted-surround, .test-notification{ border:1px dotted #ccc } diff --git a/gui/slick/css/light.css b/gui/slick/css/light.css index 20f25f3..3fdf63e 100644 --- a/gui/slick/css/light.css +++ b/gui/slick/css/light.css @@ -641,10 +641,12 @@ config*.tmpl border-bottom:1px dotted #666 } +.qr-btn .glyphicon-qrcode, .component-group-desc p{ color:#666 } +.dotted-surround, .test-notification{ border:1px dotted #ccc } diff --git a/gui/slick/css/style.css b/gui/slick/css/style.css index b4744ea..216abce 100644 --- a/gui/slick/css/style.css +++ b/gui/slick/css/style.css @@ -3029,6 +3029,7 @@ select .selected:before{ margin:0 !important } +.dotted-surround, .test-notification{ padding:5px; margin-bottom:10px; @@ -3151,6 +3152,18 @@ select .selected:before{ background-position:-104px 0 } +#api-keys > div{display:inline-block} +#api-keys span{float:left} +#api-keys span, #generate-result{line-height:22px} +#api-keys .api-key{width:235px} +#api-keys .app-name{width:135px} +.qr-btn{margin-right:6px} +.qr-btn .glyphicon-qrcode{cursor:pointer;font-size:15px} +.apikey-qr-dlg .qr-title{padding:0 25px 5px 25px;text-align:right} +.apikey-qr-dlg .qr-title em{color:#999;font-weight:bolder} +.apikey-qr-dlg .qr-title span{color:#333} +.apikey-qr-dlg .qr-body{padding:25px 25px 0} + /* ======================================================================= config_postProcessing.tmpl ========================================================================== */ diff --git a/gui/slick/interfaces/default/apiBuilder.tmpl b/gui/slick/interfaces/default/apiBuilder.tmpl index c07cb30..c84068b 100644 --- a/gui/slick/interfaces/default/apiBuilder.tmpl +++ b/gui/slick/interfaces/default/apiBuilder.tmpl @@ -34,6 +34,12 @@ addListGroup("api", "Command"); addOption("Command", "SickGear", "?cmd=sg", 1); //make default addOption("Command", "SickBeard", "?cmd=sb"); addOption("Command", "List Commands", "?cmd=listcommands"); +addList("Command", "Help", "?cmd=help", "sg.functions-list", "","", "action"); +#from sickbeard.webapi import _functionMaper +#from six import iterkeys +#for $k in sorted(iterkeys(_functionMaper), key=lambda x: x.replace('sg.', '').replace('sb.', '')) +addOption("sg.functions-list", "$k", "&subject=$k") +#end for addList("Command", "SickBeard.AddRootDir", "?cmd=sb.addrootdir", "sb.addrootdir", "", "", "action"); addList("Command", "SickGear.AddRootDir", "?cmd=sg.addrootdir", "sb.addrootdir", "", "", "action"); addOption("Command", "SickBeard.CheckScheduler", "?cmd=sb.checkscheduler", "", "", "action"); @@ -53,7 +59,7 @@ addList("Command", "SickGear.GetIndexers", "?cmd=sg.getindexers", "listindexers" addList("Command", "SickGear.GetIndexerIcon", "?cmd=sg.getindexericon", "getindexericon", "", "action"); addList("Command", "SickGear.GetNetworkIcon", "?cmd=sg.getnetworkicon", "getnetworkicon", "", "action"); addOption("Command", "SickBeard.GetRootDirs", "?cmd=sb.getrootdirs", "", "", "action"); -addOption("Command", "SickGar.GetRootDirs", "?cmd=sg.getrootdirs", "", "", "action"); +addOption("Command", "SickGear.GetRootDirs", "?cmd=sg.getrootdirs", "", "", "action"); addList("Command", "SickBeard.PauseBacklog", "?cmd=sb.pausebacklog", "sb.pausebacklog", "", "", "action"); addList("Command", "SickGear.PauseBacklog", "?cmd=sg.pausebacklog", "sb.pausebacklog", "", "", "action"); addOption("Command", "SickBeard.Ping", "?cmd=sb.ping", "", "", "action"); @@ -63,7 +69,9 @@ addOption("Command", "SickGear.Restart", "?cmd=sg.restart", "", "", "action"); addList("Command", "SickBeard.SearchTVDB", "?cmd=sb.searchtvdb", "sb.searchtvdb", "", "", "action"); addList("Command", "SickGear.SearchTV", "?cmd=sg.searchtv", "sg.searchtv", "", "", "action"); addList("Command", "SickBeard.SetDefaults", "?cmd=sb.setdefaults", "sb.setdefaults", "", "", "action"); +addList("Command", "SickGear.SetDefaults", "?cmd=sg.setdefaults", "sb.setdefaults", "", "", "action"); addOption("Command", "SickBeard.Shutdown", "?cmd=sb.shutdown", "", "", "action"); +addOption("Command", "SickGear.Shutdown", "?cmd=sg.shutdown", "", "", "action"); addList("Command", "SickGear.ListIgnoreWords", "?cmd=sg.listignorewords", "listignorewords", "", "action"); addList("Command", "SickGear.SetIgnoreWords", "?cmd=sg.setignorewords", "setwords", "", "action"); addList("Command", "SickGear.ListRequiredWords", "?cmd=sg.listrequiredwords", "listrequiredwords", "", "action"); @@ -101,7 +109,7 @@ addList("Command", "SickGear.Show.AddNew", "?cmd=sg.show.addnew", "sg.show.addne addList("Command", "Show.Cache", "?cmd=show.cache", "indexerid", "", "", "action"); addList("Command", "SickGear.Show.Cache", "?cmd=sg.show.cache", "sg.indexerid", "", "", "action"); addList("Command", "Show.Delete", "?cmd=show.delete", "indexerid", "", "", "action"); -addList("Command", "SickGear.Show.Delete", "?cmd=sg.show.delete", "sg.indexerid", "", "", "action"); +addList("Command", "SickGear.Show.Delete", "?cmd=sg.show.delete", "show-delete", "", "", "action"); addList("Command", "Show.GetBanner", "?cmd=show.getbanner", "indexerid", "", "", "action"); addList("Command", "SickGear.Show.GetBanner", "?cmd=sg.show.getbanner", "sg.indexerid", "", "", "action"); addList("Command", "SickGear.Show.ListFanart", "?cmd=sg.show.listfanart", "sg.indexerid", "", "", "action"); @@ -483,6 +491,14 @@ addOption("episode-status", "Skipped", "&status=skipped"); addOption("episode-status", "Archived", "&status=archived"); addOption("episode-status", "Ignored", "&status=ignored"); +#for $curShow in $sortedShowList: +addList("show-delete", "$curShow.name", "&indexer=$curShow.indexer&indexerid=$curShow.indexerid", "delete-options"); +#end for + +addOption("delete-options", "Optional Param", "", 1) +addList("delete-options", "Keep Files/Folders", "&full=0") +addList("delete-options", "Delete Files/Folders", "&full=1") + addOption("future", "Optional Param", "", 1); addList("future", "Sort by Date", "&sort=date", "future-type"); addList("future", "Sort by Network", "&sort=network", "future-type"); diff --git a/gui/slick/interfaces/default/config_general.tmpl b/gui/slick/interfaces/default/config_general.tmpl index 9de1797..d609894 100644 --- a/gui/slick/interfaces/default/config_general.tmpl +++ b/gui/slick/interfaces/default/config_general.tmpl @@ -496,7 +496,7 @@ API enable -

permit the use of the SickGear (and Legacy SickBeard) API

+

permit the use of the SickGear (and legacy SickBeard) API

@@ -505,10 +505,22 @@ diff --git a/gui/slick/js/apibuilder.js b/gui/slick/js/apibuilder.js index 527fb26..abd1e26 100644 --- a/gui/slick/js/apibuilder.js +++ b/gui/slick/js/apibuilder.js @@ -26,7 +26,7 @@ function goListGroup(apikey, L8, L7, L6, L5, L4, L3, L2, L1){ }); // handle the show.getposter / show.getbanner differently as they return an image and not json - if (L1 == "?cmd=sg.getnetworkicon" || L1 == "?cmd=sg.show.getposter" || L1 == "?cmd=sg.show.getbanner" || L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner" || L1 == "?cmd=sg.getindexericon") { + if (L1 == "?cmd=sg.getnetworkicon" || L1 == "?cmd=sg.show.getposter" || L1 == "?cmd=sg.show.getbanner" || L1 == "?cmd=show.getposter" || L1 == "?cmd=show.getbanner" || L1 == "?cmd=sg.getindexericon" || L1 == "?cmd=sg.show.getfanart") { var imgcache = sbRoot + "/api/" + apikey + "/" + L1 + L2 + GlobalOptions; var html = imgcache + '

'; $('#apiResponse').html(html); diff --git a/gui/slick/js/config.js b/gui/slick/js/config.js index a0f3780..23d8c66 100644 --- a/gui/slick/js/config.js +++ b/gui/slick/js/config.js @@ -232,16 +232,100 @@ $(document).ready(function () { } }); - $('#api_key').click(function () {$('#api_key').select()}); - $('#generate_new_apikey').click(function () { - $.get(sbRoot + '/config/general/generateKey', + var addQR = function(){ + if (0 < $('a[rel=qr]').length) { + var fancy = sbRoot + '/js/fancybox/jquery.fancybox'; + $.getScript(fancy + '.js', function () { + var head$ = $('head'); + if (!head$.find('link[href*="fancybox"]').length){ + head$.append(''); + !function(t,r){"object"==typeof exports&&"object"==typeof module?module.exports=r():"function"==typeof define&&define.amd?define("jquery-qrcode",[],r):"object"==typeof exports?exports["jquery-qrcode"]=r():t["jquery-qrcode"]=r()}("undefined"!=typeof self?self:this,function(){return function(e){var n={};function o(t){if(n[t])return n[t].exports;var r=n[t]={i:t,l:!1,exports:{}};return e[t].call(r.exports,r,r.exports,o),r.l=!0,r.exports}return o.m=e,o.c=n,o.d=function(t,r,e){o.o(t,r)||Object.defineProperty(t,r,{enumerable:!0,get:e})},o.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},o.t=function(r,t){if(1&t&&(r=o(r)),8&t)return r;if(4&t&&"object"==typeof r&&r&&r.__esModule)return r;var e=Object.create(null);if(o.r(e),Object.defineProperty(e,"default",{enumerable:!0,value:r}),2&t&&"string"!=typeof r)for(var n in r)o.d(e,n,function(t){return r[t]}.bind(null,n));return e},o.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return o.d(r,"a",r),r},o.o=function(t,r){return Object.prototype.hasOwnProperty.call(t,r)},o.p="",o(o.s=0)}([function(v,t,p){(function(t){function c(t){return t&&"string"==typeof t.tagName&&"IMG"===t.tagName.toUpperCase()}function a(t,r,e,n){var o={},i=p(2);i.stringToBytes=i.stringToBytesFuncs["UTF-8"];var a=i(e,r);a.addData(t),a.make(),n=n||0;var u=a.getModuleCount(),s=u+2*n;return o.text=t,o.level=r,o.version=e,o.module_count=s,o.is_dark=function(t,r){return r-=n,0<=(t-=n)&&t")[0].getContext("2d");i.font=o;var a=i.measureText(e.label).width,u=e.mSize,f=a/n,c=(1-f)*e.mPosX,l=(1-u)*e.mPosY,g=c+f,s=l+u;1===e.mode?t.add_blank(0,l-.01,n,s+.01):t.add_blank(c-.01,l-.01,.01+g,s+.01),r.fillStyle=e.fontcolor,r.font=o,r.fillText(e.label,c*n,l*n+.75*e.mSize*n)}(t,r,e):!c(e.image)||3!==n&&4!==n||function(t,r,e){var n=e.size,o=e.image.naturalWidth||1,i=e.image.naturalHeight||1,a=e.mSize,u=a*o/i,f=(1-u)*e.mPosX,c=(1-a)*e.mPosY,l=f+u,g=c+a;3===e.mode?t.add_blank(0,c-.01,n,g+.01):t.add_blank(f-.01,c-.01,.01+l,g+.01),r.drawImage(e.image,f*n,c*n,u*n,a*n)}(t,r,e)}function l(t,r,e,n,o,i,a,u){t.is_dark(a,u)&&r.rect(n,o,i,i)}function g(t,r,e,n,o,i,a,u){var f=t.is_dark,c=n+i,l=o+i,g=e.radius*i,s=a-1,h=a+1,d=u-1,v=u+1,p=f(a,u),w=f(s,d),y=f(s,u),m=f(s,v),b=f(a,v),k=f(h,v),C=f(h,u),B=f(h,d),x=f(a,d);p?function(t,r,e,n,o,i,a,u,f,c){a?t.moveTo(r+i,e):t.moveTo(r,e),u?(t.lineTo(n-i,e),t.arcTo(n,e,n,o,i)):t.lineTo(n,e),f?(t.lineTo(n,o-i),t.arcTo(n,o,r,o,i)):t.lineTo(n,o),c?(t.lineTo(r+i,o),t.arcTo(r,o,r,e,i)):t.lineTo(r,o),a?(t.lineTo(r,e+i),t.arcTo(r,e,n,e,i)):t.lineTo(r,e)}(r,n,o,c,l,g,!y&&!x,!y&&!b,!C&&!b,!C&&!x):function(t,r,e,n,o,i,a,u,f,c){a&&(t.moveTo(r+i,e),t.lineTo(r,e),t.lineTo(r,e+i),t.arcTo(r,e,r+i,e,i)),u&&(t.moveTo(n-i,e),t.lineTo(n,e),t.lineTo(n,e+i),t.arcTo(n,e,n-i,e,i)),f&&(t.moveTo(n-i,o),t.lineTo(n,o),t.lineTo(n,o-i),t.arcTo(n,o,n-i,o,i)),c&&(t.moveTo(r+i,o),t.lineTo(r,o),t.lineTo(r,o-i),t.arcTo(r,o,r+i,o,i))}(r,n,o,c,l,g,y&&x&&w,y&&b&&m,C&&b&&k,C&&x&&B)}function n(t,r){var e=h(r.text,r.ecLevel,r.minVersion,r.maxVersion,r.quiet);if(!e)return null;var n=d(t).data("qrcode",e),o=n[0].getContext("2d");return i(e,o,r),function(t,r,e){var n,o,i=t.module_count,a=e.size/i,u=l;for(0").attr("width",t.size).attr("height",t.size);return n(r,t)}function o(t){return f&&"canvas"===t.render?r(t):f&&"image"===t.render?function(t){return d("").attr("src",r(t)[0].toDataURL("image/png"))}(t):function(t){var r=h(t.text,t.ecLevel,t.minVersion,t.maxVersion,t.quiet);if(!r)return null;var e,n,o=t.size,i=t.background,a=Math.floor,u=r.module_count,f=a(o/u),c=a(.5*(o-f*u)),l={position:"relative",left:0,top:0,padding:0,margin:0,width:o,height:o},g={position:"absolute",padding:0,margin:0,width:f,height:f,"background-color":t.fill},s=d("
").data("qrcode",r).css(l);for(i&&s.css("background-color",i),e=0;e").css(g).css({left:c+n*f,top:c+e*f}).appendTo(s);return s}(t)}var e,u=t.window,d=u.jQuery,f=!(!(e=u.document.createElement("canvas")).getContext||!e.getContext("2d")),s={render:"canvas",minVersion:1,maxVersion:40,ecLevel:"L",left:0,top:0,size:200,fill:"#000",background:"#fff",text:"no text",radius:0,quiet:0,mode:0,mSize:.1,mPosX:.5,mPosY:.5,label:"no label",fontname:"sans",fontcolor:"#000",image:null};d.fn.qrcode=v.exports=function(t){var e=d.extend({},s,t);return this.each(function(t,r){"canvas"===r.nodeName.toLowerCase()?n(r,e):d(r).append(o(e))})}}).call(this,p(1))},function(t,r){var e;e=function(){return this}();try{e=e||new Function("return this")()}catch(t){"object"==typeof window&&(e=window)}t.exports=e},function(t,r,e){var n,o,i,a=function(){function i(t,r){function a(t,r){l=function(t){for(var r=new Array(t),e=0;e>e&1);l[Math.floor(e/3)][e%3+g-8-3]=n}for(e=0;e<18;e+=1){n=!t&&1==(r>>e&1);l[e%3+g-8-3][Math.floor(e/3)]=n}},d=function(t,r){for(var e=f<<3|r,n=y.getBCHTypeInfo(e),o=0;o<15;o+=1){var i=!t&&1==(n>>o&1);o<6?l[o][8]=i:o<8?l[o+1][8]=i:l[g-15+o][8]=i}for(o=0;o<15;o+=1){i=!t&&1==(n>>o&1);o<8?l[8][g-o-1]=i:o<9?l[8][15-o-1+1]=i:l[8][15-o-1]=i}l[g-8][8]=!t},v=function(t,r){for(var e=-1,n=g-1,o=7,i=0,a=y.getMaskFunction(r),u=g-1;0>>o&1)),a(n,u-f)&&(c=!c),l[n][u-f]=c,-1==(o-=1)&&(i+=1,o=7)}if((n+=e)<0||g<=n){n-=e,e=-e;break}}},p=function(t,r,e){for(var n=C.getRSBlocks(t,r),o=B(),i=0;i8*u)throw"code length overflow. ("+o.getLengthInBits()+">"+8*u+")";for(o.getLengthInBits()+4<=8*u&&o.put(0,4);o.getLengthInBits()%8!=0;)o.putBit(!1);for(;!(o.getLengthInBits()>=8*u||(o.put(236,8),o.getLengthInBits()>=8*u));)o.put(17,8);return function(t,r){for(var e=0,n=0,o=0,i=new Array(r.length),a=new Array(r.length),u=0;u',e+="";for(var n=0;n";for(var o=0;o';e+=""}return e+="",e+=""},h.createSvgTag=function(t,r){var e={};"object"==typeof t&&(t=(e=t).cellSize,r=e.margin),t=t||2,r=void 0===r?4*t:r;var n,o,i,a,u=h.getModuleCount()*t+2*r,f="";for(a="l"+t+",0 0,"+t+" -"+t+",0 0,-"+t+"z ",f+='>>8),r.push(255&o)):r.push(a)}}return r}};var a=1,u=2,o=4,f=8,w={L:1,M:0,Q:3,H:2},n=0,c=1,l=2,g=3,s=4,h=5,d=6,v=7,y=function(){function e(t){for(var r=0;0!=t;)r+=1,t>>>=1;return r}var r=[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],t={};return t.getBCHTypeInfo=function(t){for(var r=t<<10;0<=e(r)-e(1335);)r^=1335<>>8)},writeBytes:function(t,r,e){r=r||0,e=e||t.length;for(var n=0;n>>7-t%8&1)},put:function(t,r){for(var e=0;e>>r-e-1&1))},getLengthInBits:function(){return n},putBit:function(t){var r=Math.floor(n/8);e.length<=r&&e.push(0),t&&(e[r]|=128>>>n%8),n+=1}};return o},x=function(t){var r=a,n=t,e={getMode:function(){return r},getLength:function(t){return n.length},write:function(t){for(var r=n,e=0;e+2>>8&255)+(255&n),t.put(n,13),e+=2}if(e=e.length){if(0==i)return-1;throw"unexpected end of file./"+i}var t=e.charAt(n);if(n+=1,"="==t)return i=0,-1;t.match(/^\s$/)||(o=o<<6|a(t.charCodeAt(0)),i+=6)}var r=o>>>i-8&255;return i-=8,r}},a=function(t){if(65<=t&&t<=90)return t-65;if(97<=t&&t<=122)return t-97+26;if(48<=t&&t<=57)return t-48+52;if(43==t)return 62;if(47==t)return 63;throw"c:"+t};return r},L=function(t,r,e){for(var n=function(t,r){var n=t,o=r,g=new Array(t*r),e={setPixel:function(t,r,e){g[r*n+t]=e},write:function(t){t.writeString("GIF87a"),t.writeShort(n),t.writeShort(o),t.writeByte(128),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(0),t.writeByte(255),t.writeByte(255),t.writeByte(255),t.writeString(","),t.writeShort(0),t.writeShort(0),t.writeShort(n),t.writeShort(o),t.writeByte(0);var r=i(2);t.writeByte(2);for(var e=0;255>>r!=0)throw"length over";for(;8<=n+r;)e.writeByte(255&(t<>>=8-n,n=o=0;o|=t<>>o-6),o-=6},t.flush=function(){if(0>6,128|63&n):n<55296||57344<=n?r.push(224|n>>12,128|n>>6&63,128|63&n):(e++,n=65536+((1023&n)<<10|1023&t.charCodeAt(e)),r.push(240|n>>18,128|n>>12&63,128|n>>6&63,128|63&n))}return r}(t)},o=[],void 0===(i="function"==typeof(n=function(){return a})?n.apply(r,o):n)||(t.exports=i)}])}); + } + $('a[rel=qr]').fancybox({ + padding: 0, + minWidth: 350, + minHeight: 350, + helpers: { + title: null, + overlay: { + locked: false, + css: {'background': 'rgba(0, 0, 0, 0.4)'} + } + }, + wrapCSS: 'apikey-qr-dlg', + afterLoad: function() { + var row = $(this.element).closest('div'); + this.content = '
App name: ' + row.find('.app-name').text() + '
'; + this.inner.prepend('
'); + $('.apikey-qr-dlg').find('.qr-body').html('').qrcode( + {render: 'image', fill: '#333', radius: 0.5, minVersion: 6, background: null, size: 300, text: row.find('.api-key').text() }); + } + }); + }); + } + }; + addQR(); + var generateApiKey = function(){ + var appName$ = $('#app-name'); + appName$.removeClass('warning'); + $('#generate-result').html(' ').removeClass('dotted-surround'); + $.getJSON(sbRoot + '/config/general/create_apikey', {app_name: appName$.val()}, function (data) { - if (data.error != undefined) { + if (undefined === data.error) { + var appInput$ = $('#app-name'); + if (undefined === data.added) { + appInput$.addClass('warning'); + $('#generate-result').html(data.result).addClass('dotted-surround'); + } else { + $('#tip-addkeys').hide(); + var newRow$ = $('#api-keys') + .append($('.new-key').first().clone(!0)) + .find('.new-key').last().removeClass('new-key'); + newRow$.find('.api-key').text(data.added); + newRow$.find('.app-name').text(appInput$.val()); + newRow$.find('a').attr('rel', 'qr'); + appInput$.val(''); + newRow$.show(); + addQR(); + } + } else { alert(data.error); - return; } - $('#api_key').val(data); }); + }; + $('#app-name').keypress(function(ev){ + if (13 === ev.which) { + ev.preventDefault(); + generateApiKey(); + return !1; + } + }); + $('#generate-api-key').on('click', generateApiKey); + $('.revoke').on('click', function(){ + $('#app-name').removeClass('warning'); + $('#generate-result').html(' ').removeClass('dotted-surround'); + var row$ = $(this).closest('div'), appName = row$.find('.app-name').text(); + if (confirm('Revoke "' + appName + '" apikey?')) { + $.getJSON(sbRoot + '/config/general/revoke_apikey', { + app_name: appName, + api_key: row$.find('.api-key').text()}, + function (data) { + if (undefined === data.error) { + if (undefined === data.removed) { + $('#app-name').addClass('warning'); + $('#generate-result').html(data.result).addClass('dotted-surround'); + } else { + row$.remove(); + if (!$('#api-keys').find('div:visible').length) { + $('#tip-addkeys').show(); + } + } + } else { + alert(data.error); + } + } + ); + } }); $('#branchCheckout').click(function () { diff --git a/lib/imdbpie/imdbpie.py b/lib/imdbpie/imdbpie.py index 99b83a4..b268e64 100644 --- a/lib/imdbpie/imdbpie.py +++ b/lib/imdbpie/imdbpie.py @@ -53,12 +53,15 @@ _SIMPLE_GET_ENDPOINTS = { class Imdb(Auth): - def __init__(self, locale=None, exclude_episodes=False, session=None): + def __init__(self, locale=None, exclude_episodes=False, session=None, cachedir=None): self.locale = locale or 'en_US' self.region = self.locale.split('_')[-1].upper() self.exclude_episodes = exclude_episodes self.session = session or requests.Session() - self._cachedir = tempfile.gettempdir() + if not cachedir: + self._cachedir = tempfile.gettempdir() + else: + self._cachedir = cachedir def __getattr__(self, name): if name in _SIMPLE_GET_ENDPOINTS: diff --git a/sickbeard/__init__.py b/sickbeard/__init__.py index bcf77ea..0473974 100755 --- a/sickbeard/__init__.py +++ b/sickbeard/__init__.py @@ -146,7 +146,7 @@ CPU_PRESET = 'DISABLED' ANON_REDIRECT = None USE_API = False -API_KEY = None +API_KEYS = [] ENABLE_HTTPS = False HTTPS_CERT = None @@ -619,7 +619,7 @@ def init_stage_1(console_logging): global THEME_NAME, DEFAULT_HOME, FANART_LIMIT, SHOWLIST_TAGVIEW, SHOW_TAGS, \ HOME_SEARCH_FOCUS, USE_IMDB_INFO, IMDB_ACCOUNTS, DISPLAY_FREESPACE, SORT_ARTICLE, FUZZY_DATING, TRIM_ZERO, \ DATE_PRESET, TIME_PRESET, TIME_PRESET_W_SECONDS, TIMEZONE_DISPLAY, \ - WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEY, WEB_PORT, WEB_LOG, \ + WEB_USERNAME, WEB_PASSWORD, CALENDAR_UNPROTECTED, USE_API, API_KEYS, WEB_PORT, WEB_LOG, \ ENABLE_HTTPS, HTTPS_CERT, HTTPS_KEY, WEB_IPV6, WEB_IPV64, HANDLE_REVERSE_PROXY, \ SEND_SECURITY_HEADERS, ALLOWED_HOSTS # Gen Config/Advanced @@ -814,7 +814,11 @@ def init_stage_1(console_logging): TRASH_ROTATE_LOGS = bool(check_setting_int(CFG, 'General', 'trash_rotate_logs', 0)) USE_API = bool(check_setting_int(CFG, 'General', 'use_api', 0)) - API_KEY = check_setting_str(CFG, 'General', 'api_key', '') + API_KEYS = [k.split(':::') for k in check_setting_str(CFG, 'General', 'api_keys', '').split('|||') if k] + if not API_KEYS: + tmp_api_key = check_setting_str(CFG, 'General', 'api_key', None) + if None is not tmp_api_key: + API_KEYS = [['app name (old key)', tmp_api_key]] DEBUG = bool(check_setting_int(CFG, 'General', 'debug', 0)) @@ -1668,7 +1672,7 @@ def save_config(): new_config['General']['cpu_preset'] = CPU_PRESET new_config['General']['anon_redirect'] = ANON_REDIRECT new_config['General']['use_api'] = int(USE_API) - new_config['General']['api_key'] = API_KEY + new_config['General']['api_keys'] = '|||'.join([':::'.join(a) for a in API_KEYS]) new_config['General']['debug'] = int(DEBUG) new_config['General']['enable_https'] = int(ENABLE_HTTPS) new_config['General']['https_cert'] = HTTPS_CERT diff --git a/sickbeard/indexermapper.py b/sickbeard/indexermapper.py index d66bfc8..5549546 100644 --- a/sickbeard/indexermapper.py +++ b/sickbeard/indexermapper.py @@ -17,6 +17,7 @@ import datetime import re import traceback +import os import requests import sickbeard @@ -26,7 +27,7 @@ from lib.dateutil.parser import parse from lib.unidecode import unidecode from libtrakt import TraktAPI from libtrakt.exceptions import TraktAuthException, TraktException -from sickbeard import db, logger +from sickbeard import db, logger, encodingKludge as ek from sickbeard.helpers import tryInt, getURL from sickbeard.indexers.indexer_config import (INDEXER_TVDB, INDEXER_TVRAGE, INDEXER_TVMAZE, INDEXER_IMDB, INDEXER_TRAKT, INDEXER_TMDB) @@ -198,7 +199,7 @@ def get_trakt_ids(url_trakt): def get_imdbid_by_name(name, startyear): ids = {} try: - res = Imdb(exclude_episodes=True).search_for_title(title=name) + res = Imdb(exclude_episodes=True, cachedir=ek.ek(os.path.join, sickbeard.CACHE_DIR, 'imdb-pie')).search_for_title(title=name) for r in res: if isinstance(r.get('type'), basestring) and 'tv series' == r.get('type').lower() \ and str(startyear) == str(r.get('year')): diff --git a/sickbeard/tv.py b/sickbeard/tv.py index 24fd232..18714c8 100644 --- a/sickbeard/tv.py +++ b/sickbeard/tv.py @@ -1116,7 +1116,7 @@ class TVShow(object): self._imdbid = redirect_check imdb_id = redirect_check imdb_info['imdb_id'] = self.imdbid - i = imdbpie.Imdb(exclude_episodes=True) + i = imdbpie.Imdb(exclude_episodes=True, cachedir=ek.ek(os.path.join, sickbeard.CACHE_DIR, 'imdb-pie')) if not re.search(r'tt\d{7}', imdb_id, flags=re.I): logger.log('Not a valid imdbid: %s for show: %s' % (imdb_id, self.name), logger.WARNING) return diff --git a/sickbeard/webapi.py b/sickbeard/webapi.py index 04126d3..c146d13 100644 --- a/sickbeard/webapi.py +++ b/sickbeard/webapi.py @@ -46,9 +46,11 @@ from sickbeard.scene_numbering import set_scene_numbering_helper from common import Quality, qualityPresetStrings, statusStrings from sickbeard.indexers.indexer_config import * from sickbeard.indexers import indexer_config, indexer_api +from sickbeard.tv import TVShow, TVEpisode from tornado import gen from sickbeard.search_backlog import FORCED_BACKLOG from sickbeard.webserve import NewHomeAddShows +from six import iteritems, integer_types try: import json @@ -92,6 +94,13 @@ quality_map = {'sdtv': Quality.SDTV, quality_map_inversed = {v: k for k, v in quality_map.iteritems()} +def api_log(obj, msg, level=logger.MESSAGE): + apikey_name = getattr(obj, 'apikey_name', '') + if apikey_name: + apikey_name = ' (%s)' % apikey_name + logger.log('%s%s' % ('API%s:: ' % apikey_name, msg), level) + + class ApiServerLoading(webserve.BaseHandler): @gen.coroutine def get(self, route, *args, **kwargs): @@ -104,12 +113,16 @@ class PythonObjectEncoder(json.JSONEncoder): def default(self, obj): if isinstance(obj, set): return list(obj) + elif isinstance(obj, TVEpisode): + return {'season': obj.season, 'episode': obj.episode} + elif isinstance(obj, TVShow): + return {'name': obj.name, 'indexer': obj.indexer, 'indexer_id': obj.indexerid} return json.JSONEncoder.default(self, obj) class Api(webserve.BaseHandler): """ api class that returns json results """ - version = 10 # use an int since float-point is unpredictible + version = 11 # use an int since float-point is unpredictible intent = 4 def check_xsrf_cookie(self): @@ -147,8 +160,8 @@ class Api(webserve.BaseHandler): args = args[1:] - self.apiKey = sickbeard.API_KEY - access, accessMsg, args, kwargs = self._grand_access(self.apiKey, route, args, kwargs) + self.apiKeys = sickbeard.API_KEYS + access, accessMsg, args, kwargs = self._grand_access(self.apiKeys, route, args, kwargs) # set the output callback # default json @@ -158,9 +171,9 @@ class Api(webserve.BaseHandler): # do we have acces ? if access: - logger.log(accessMsg, logger.DEBUG) + api_log(self, accessMsg, logger.DEBUG) else: - logger.log(accessMsg, logger.WARNING) + api_log(self, accessMsg, logger.WARNING) return outputCallbackDict['default'](_responds(RESULT_DENIED, msg=accessMsg)) # set the original call_dispatcher as the local _call_dispatcher @@ -181,7 +194,7 @@ class Api(webserve.BaseHandler): try: outDict = _call_dispatcher(self, args, kwargs) except Exception as e: # real internal error oohhh nooo :( - logger.log(u"API :: " + ex(e), logger.ERROR) + api_log(self, ex(e), logger.ERROR) errorData = {"error_msg": ex(e), "args": args, "kwargs": kwargs} @@ -203,28 +216,30 @@ class Api(webserve.BaseHandler): out = '%s(%s);' % (callback, out) # wrap with JSONP call if requested except Exception as e: # if we fail to generate the output fake an error - logger.log(u'API :: ' + traceback.format_exc(), logger.ERROR) + api_log(self, traceback.format_exc(), logger.ERROR) out = '{"result":"' + result_type_map[RESULT_ERROR] + '", "message": "error while composing output: "' + ex( e) + '"}' return out - def _grand_access(self, realKey, apiKey, args, kwargs): + def _grand_access(self, realKeys, apiKey, args, kwargs): """ validate api key and log result """ remoteIp = self.request.remote_ip + self.apikey_name = '' if not sickbeard.USE_API: - msg = u'API :: ' + remoteIp + ' - SB API Disabled. ACCESS DENIED' - return False, msg, args, kwargs - elif apiKey == realKey: - msg = u'API :: ' + remoteIp + ' - gave correct API KEY. ACCESS GRANTED' - return True, msg, args, kwargs - elif not apiKey: - msg = u'API :: ' + remoteIp + ' - gave NO API KEY. ACCESS DENIED' + msg = u'%s - SB API Disabled. ACCESS DENIED' % remoteIp return False, msg, args, kwargs - else: - msg = u'API :: ' + remoteIp + ' - gave WRONG API KEY ' + apiKey + '. ACCESS DENIED' + if not apiKey: + msg = u'%s - gave NO API KEY. ACCESS DENIED' % remoteIp return False, msg, args, kwargs + for realKey in realKeys: + if apiKey == realKey[1]: + self.apikey_name = realKey[0] + msg = u'%s - gave correct API KEY: %s. ACCESS GRANTED' % (remoteIp, realKey[0]) + return True, msg, args, kwargs + msg = u'%s - gave WRONG API KEY %s. ACCESS DENIED' % (remoteIp, apiKey) + return False, msg, args, kwargs def call_dispatcher(handler, args, kwargs): @@ -233,10 +248,6 @@ def call_dispatcher(handler, args, kwargs): or calls the TVDBShorthandWrapper when the first args element is a number or returns an error that there is no such cmd """ - logger.log(u"API :: all args: '" + str(args) + "'", logger.DEBUG) - logger.log(u"API :: all kwargs: '" + str(kwargs) + "'", logger.DEBUG) - # logger.log(u"API :: dateFormat: '" + str(dateFormat) + "'", logger.DEBUG) - cmds = None if args: cmds = args[0] @@ -246,6 +257,12 @@ def call_dispatcher(handler, args, kwargs): cmds = kwargs["cmd"] del kwargs["cmd"] + api_log(handler, u"cmd: '" + str(cmds) + "'", logger.DEBUG) + api_log(handler, u"all args: '" + str(args) + "'", logger.DEBUG) + api_log(handler, u"all kwargs: '" + str(kwargs) + "'", logger.DEBUG) + # logger.log(u"dateFormat: '" + str(dateFormat) + "'", logger.DEBUG) + + outDict = {} if cmds != None: cmds = cmds.split("|") @@ -256,7 +273,7 @@ def call_dispatcher(handler, args, kwargs): if len(cmd.split("_")) > 1: # was a index used for this cmd ? cmd, cmdIndex = cmd.split("_") # this gives us the clear cmd and the index - logger.log(u"API :: " + cmd + ": curKwargs " + str(curKwargs), logger.DEBUG) + api_log(handler, cmd + ": curKwargs " + str(curKwargs), logger.DEBUG) if not (multiCmds and cmd in ('show.getposter', 'show.getbanner')): # skip these cmd while chaining try: if cmd in _functionMaper: @@ -346,6 +363,12 @@ class ApiCall(object): # old sickbeard call self._sickbeard_call = getattr(self, '_sickbeard_call', False) + if 'help' not in kwargs and self._sickbeard_call: + call_name = _functionMaper_reversed.get(self.__class__, '') + if 'sb' != call_name: + self.log('SickBeard API call "%s" should be replaced with SickGear API "%s" calls to get much ' + 'improved detail and functionality, contact your App developer and ask them to update ' + 'their code.' % (call_name, self._get_old_command()), logger.WARNING) @property def sickbeard_call(self): @@ -361,6 +384,24 @@ class ApiCall(object): # override with real output function in subclass return {} + def log(self, msg, level=logger.MESSAGE): + api_log(self.handler, msg, level) + + def _get_old_command(self, command_class=None): + c_class = command_class or self + new_call_name = None + help = getattr(c_class, '_help', None) + if getattr(c_class, '_sickbeard_call', False) or "SickGearCommand" in help: + call_name = _functionMaper_reversed.get(c_class.__class__, '') + new_call_name = 'sg.%s' % call_name.replace('sb.', '') if 'sb' != call_name else 'sg' + if new_call_name not in _functionMaper: + if isinstance(help, dict) and "SickGearCommand" in help \ + and help['SickGearCommand'] in _functionMaper: + new_call_name = help['SickGearCommand'] + else: + new_call_name = 'sg.*' + return new_call_name + def return_help(self): try: if self._requiredParams: @@ -403,6 +444,13 @@ class ApiCall(object): msg = "The required parameter: '" + self._missing[0] + "' was not set" else: msg = "The required parameters: '" + "','".join(self._missing) + "' where not set" + try: + remote_ip = self.handler.request.remote_ip + except (BaseException, Exception): + remote_ip = '"unknown ip"' + self.log("API call from host %s triggers :: %s: %s" % + (remote_ip, _functionMaper_reversed.get(self.__class__, ''), msg), + logger.ERROR) return _responds(RESULT_ERROR, msg=msg) def check_params(self, args, kwargs, key, default, required, type, allowedValues, sub_type=None): @@ -516,7 +564,7 @@ class ApiCall(object): elif type == "ignore": pass else: - logger.log(u"API :: Invalid param type set " + str(type) + " can not check or convert ignoring it", + self.log(u"Invalid param type set " + str(type) + " can not check or convert ignoring it", logger.ERROR) if error: @@ -765,13 +813,14 @@ class CMD_ListCommands(ApiCall): color = ("", " style='color: grey !important;'")[is_old_command] out += '

%s%s

' % (color, f, ("", " (Sickbeard compatibility command)")[is_old_command]) if isinstance(help, dict): - sg_c = '' - if "SickGearCommand" in help: - sg_c += '%s' % help['SickGearCommand'] - out += "

for all features use SickGear API Command: %s

" % help['SickGearCommand'] + sg_cmd_new = self._get_old_command(command_class=v) + sg_cmd = '' + if sg_cmd_new: + sg_cmd = '%s' % sg_cmd_new + out += "

for all features use SickGear API Command: %s

" % sg_cmd_new if "desc" in help: if is_old_command: - table_sickbeard_commands += '%s%s' % (help['desc'], sg_c) + table_sickbeard_commands += '%s%s' % (help['desc'], sg_cmd) else: table_sickgear_commands += '%s' % help['desc'] out += help['desc'] @@ -832,7 +881,7 @@ class CMD_Help(ApiCall): def run(self): """ display help information for a given subject/command """ if self.subject in _functionMaper: - out = _responds(RESULT_SUCCESS, _functionMaper.get(self.subject)((), {"help": 1}).run()) + out = _responds(RESULT_SUCCESS, _functionMaper.get(self.subject)(None, (), {"help": 1}).run()) else: out = _responds(RESULT_FAILURE, msg="No such cmd") return out @@ -1302,7 +1351,7 @@ class CMD_SickGearEpisodeSetStatus(ApiCall): cur_backlog_queue_item = search_queue.BacklogQueueItem(showObj, segment) sickbeard.searchQueueScheduler.action.add_item(cur_backlog_queue_item) #@UndefinedVariable - logger.log(u"API :: Starting backlog for " + showObj.name + " season " + str( + self.log(u"Starting backlog for " + showObj.name + " season " + str( season) + " because some episodes were set to WANTED") extra_msg = " Backlog started" @@ -2051,11 +2100,15 @@ class CMD_SickGearForceSearch(ApiCall): def run(self): """ force the given search type search """ result = None - if 'recent' == self.searchtype: + if 'recent' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_recentsearch_in_progress() \ + and not sickbeard.recentSearchScheduler.action.amActive: result = sickbeard.recentSearchScheduler.forceRun() - elif 'backlog' == self.searchtype: - result = sickbeard.backlogSearchScheduler.force_search(force_type=FORCED_BACKLOG) - elif 'proper' == self.searchtype: + elif 'backlog' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_backlog_in_progress() \ + and not sickbeard.backlogSearchScheduler.action.amActive: + sickbeard.backlogSearchScheduler.force_search(force_type=FORCED_BACKLOG) + result = True + elif 'proper' == self.searchtype and not sickbeard.searchQueueScheduler.action.is_propersearch_in_progress() \ + and not sickbeard.properFinderScheduler.action.amActive: result = sickbeard.properFinderScheduler.forceRun() if result: return _responds(RESULT_SUCCESS, msg='%s search successfully forced' % self.searchtype) @@ -2106,7 +2159,7 @@ class CMD_SickGearGetDefaults(ApiCall): data = {"status": statusStrings[sickbeard.STATUS_DEFAULT].lower(), "flatten_folders": int(sickbeard.FLATTEN_FOLDERS_DEFAULT), "initial": anyQualities, - "archive": bestQualities, "future_show_paused": int(sickgear.EPISODE_VIEW_DISPLAY_PAUSED)} + "archive": bestQualities, "future_show_paused": int(sickbeard.EPISODE_VIEW_DISPLAY_PAUSED)} return _responds(RESULT_SUCCESS, data) @@ -2470,12 +2523,12 @@ class CMD_SickGearSearchIndexers(ApiCall): try: myShow = t[int(self.indexerid), False] except (sickbeard.indexer_shownotfound, sickbeard.indexer_error): - logger.log(u"API :: Unable to find show with id " + str(self.indexerid), logger.WARNING) + self.log(u"Unable to find show with id " + str(self.indexerid), logger.WARNING) return _responds(RESULT_SUCCESS, {"results": [], "langid": lang_id}) if not myShow.data['seriesname']: - logger.log( - u"API :: Found show with indexerid " + str(self.indexerid) + ", however it contained no show name", + self.log( + u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name", logger.DEBUG) return _responds(RESULT_FAILURE, msg="Show contains no name, invalid result") @@ -2514,7 +2567,7 @@ class CMD_SickBeardSearchIndexers(CMD_SickGearSearchIndexers): CMD_SickGearSearchIndexers.__init__(self, handler, args, kwargs) -class CMD_SickBeardSetDefaults(ApiCall): +class CMD_SickGearSetDefaults(ApiCall): _help = {"desc": "set sickgear user defaults", "optionalParameters": {"initial": {"desc": "initial quality for the show"}, "archive": {"desc": "archive quality for the show"}, @@ -2574,6 +2627,22 @@ class CMD_SickBeardSetDefaults(ApiCall): return _responds(RESULT_SUCCESS, msg="Saved defaults") +class CMD_SickBeardSetDefaults(CMD_SickGearSetDefaults): + _help = {"desc": "set sickgear user defaults", + "optionalParameters": {"initial": {"desc": "initial quality for the show"}, + "archive": {"desc": "archive quality for the show"}, + "flatten_folders": {"desc": "flatten subfolders within the show directory"}, + "status": {"desc": "status of missing episodes"} + }, + "SickGearCommand": "sg.setdefaults", + } + + def __init__(self, handler, args, kwargs): + # super, missing, help + self.sickbeard_call = True + CMD_SickGearSetDefaults.__init__(self, handler, args, kwargs) + + class CMD_SickGearSetSceneNumber(ApiCall): _help = {"desc": "set Scene Numbers", "requiredParameters": {"indexerid": {"desc": "unique id of a show"}, @@ -2647,7 +2716,7 @@ class CMD_SickGearActivateSceneNumber(ApiCall): msg="Scene Numbering %sactivated" % ('de', '')[self.activate]) -class CMD_SickBeardShutdown(ApiCall): +class CMD_SickGearShutdown(ApiCall): _help = {"desc": "shutdown sickgear"} def __init__(self, handler, args, kwargs): @@ -2662,6 +2731,16 @@ class CMD_SickBeardShutdown(ApiCall): return _responds(RESULT_SUCCESS, msg="SickGear is shutting down...") +class CMD_SickBeardShutdown(CMD_SickGearShutdown): + _help = {"desc": "shutdown sickgear", + "SickGearCommand": "sg.shutdown", + } + + def __init__(self, handler, args, kwargs): + self.sickbeard_call = True + CMD_SickGearShutdown.__init__(self, handler, args, kwargs) + + class CMD_SickGearListIgnoreWords(ApiCall): _help = {"desc": "list ignore words", "optionalParameters": {"indexerid": {"desc": "unique id of a show"}, @@ -2959,8 +3038,11 @@ class CMD_SickGearShow(ApiCall): return _responds(RESULT_FAILURE, msg="Show not found") showDict = {} - showDict["season_list"] = CMD_ShowSeasonList(self.handler, (), {"indexerid": self.indexerid}).run()["data"] - showDict["cache"] = CMD_ShowCache(self.handler, (), {"indexerid": self.indexerid}).run()["data"] + showDict["season_list"] = CMD_SickGearShowSeasonList(self.handler, (), + {"indexer": self.indexer, "indexerid": self.indexerid} + ).run()["data"] + showDict["cache"] = CMD_SickGearShowCache(self.handler, (), {"indexer": self.indexer, + "indexerid": self.indexerid}).run()["data"] genreList = [] if showObj.genre: @@ -3083,15 +3165,28 @@ class CMD_SickGearShowAddExisting(ApiCall): if not ek.ek(os.path.isdir, self.location): return _responds(RESULT_FAILURE, msg='Not a valid location') + lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy() + lINDEXER_API_PARMS['language'] = 'en' + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI + lINDEXER_API_PARMS['actors'] = False + + t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS) + + try: + myShow = t[int(self.indexerid), False] + except (sickbeard.indexer_shownotfound, sickbeard.indexer_error): + self.log(u"Unable to find show with id " + str(self.indexerid), logger.WARNING) + return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") + indexerName = None - indexerResult = CMD_SickBeardSearchIndexers(self.handler, [], - {"indexerid": self.indexerid, "indexer": self.indexer}).run() + if not myShow.data['seriesname']: + self.log( + u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name", + logger.DEBUG) + return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") - if indexerResult['result'] == result_type_map[RESULT_SUCCESS]: - if not indexerResult['data']['results']: - return _responds(RESULT_FAILURE, msg="Empty results returned, check indexerid and try again") - if len(indexerResult['data']['results']) == 1 and 'name' in indexerResult['data']['results'][0]: - indexerName = indexerResult['data']['results'][0]['name'] + else: + indexerName = myShow.data['seriesname'] if not indexerName: return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") @@ -3134,7 +3229,8 @@ class CMD_ShowAddExisting(CMD_SickGearShowAddExisting): def __init__(self, handler, args, kwargs): kwargs['indexer'] = INDEXER_TVDB # required - kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", []) + if 'tvdbid' in kwargs and 'indexerid' not in kwargs: + kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", []) # super, missing, help self.sickbeard_call = True CMD_SickGearShowAddExisting.__init__(self, handler, args, kwargs) @@ -3231,15 +3327,28 @@ class CMD_SickGearShowAddNew(ApiCall): return _responds(RESULT_FAILURE, msg="Status prohibited") newStatus = self.status + lINDEXER_API_PARMS = sickbeard.indexerApi(self.indexer).api_params.copy() + lINDEXER_API_PARMS['language'] = 'en' + lINDEXER_API_PARMS['custom_ui'] = classes.AllShowsNoFilterListUI + lINDEXER_API_PARMS['actors'] = False + + t = sickbeard.indexerApi(self.indexer).indexer(**lINDEXER_API_PARMS) + + try: + myShow = t[int(self.indexerid), False] + except (sickbeard.indexer_shownotfound, sickbeard.indexer_error): + self.log(u"Unable to find show with id " + str(self.indexerid), logger.WARNING) + return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") + indexerName = None - indexerResult = CMD_SickBeardSearchIndexers(self.handler, [], - {"indexerid": self.indexerid, "indexer": self.indexer}).run() + if not myShow.data['seriesname']: + self.log( + u"Found show with indexerid " + str(self.indexerid) + ", however it contained no show name", + logger.DEBUG) + return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") - if indexerResult['result'] == result_type_map[RESULT_SUCCESS]: - if not indexerResult['data']['results']: - return _responds(RESULT_FAILURE, msg="Empty results returned, check indexerid and try again") - if len(indexerResult['data']['results']) == 1 and 'name' in indexerResult['data']['results'][0]: - indexerName = indexerResult['data']['results'][0]['name'] + else: + indexerName = myShow.data['seriesname'] if not indexerName: return _responds(RESULT_FAILURE, msg="Unable to retrieve information from indexer") @@ -3249,11 +3358,11 @@ class CMD_SickGearShowAddNew(ApiCall): # don't create show dir if config says not to if sickbeard.ADD_SHOWS_WO_DIR: - logger.log(u"Skipping initial creation of " + showPath + " due to config.ini setting") + self.log(u"Skipping initial creation of " + showPath + " due to config.ini setting") else: dir_exists = helpers.makeDir(showPath) if not dir_exists: - logger.log(u"API :: Unable to create the folder " + showPath + ", can't add the show", logger.ERROR) + self.log(u"Unable to create the folder " + showPath + ", can't add the show", logger.ERROR) return _responds(RESULT_FAILURE, {"path": showPath}, "Unable to create the folder " + showPath + ", can't add the show") else: @@ -3292,7 +3401,8 @@ class CMD_ShowAddNew(CMD_SickGearShowAddNew): def __init__(self, handler, args, kwargs): kwargs['indexer'] = INDEXER_TVDB - kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", []) + if 'tvdbid' in kwargs and 'indexerid' not in kwargs: + kwargs['indexerid'], args = self.check_params(args, kwargs, "tvdbid", None, True, "int", []) # required # optional # super, missing, help @@ -3359,6 +3469,7 @@ class CMD_SickGearShowDelete(ApiCall): "requiredParameters": {"indexer": {"desc": "indexer of a show"}, "indexerid": {"desc": "unique id of a show"}, }, + "optionalParameters": {"full": {"desc": "delete files/folder of show"}} } def __init__(self, handler, args, kwargs): @@ -3367,6 +3478,7 @@ class CMD_SickGearShowDelete(ApiCall): [i for i in indexer_api.indexerApi().indexers]) self.indexerid, args = self.check_params(args, kwargs, "indexerid", None, True, "int", []) # optional + self.full_delete, args = self.check_params(args, kwargs, "full", False, False, "bool", []) # super, missing, help ApiCall.__init__(self, handler, args, kwargs) @@ -3380,7 +3492,7 @@ class CMD_SickGearShowDelete(ApiCall): showObj) or sickbeard.showQueueScheduler.action.isBeingUpdated(showObj): #@UndefinedVariable return _responds(RESULT_FAILURE, msg="Show can not be deleted while being added or updated") - showObj.deleteShow() + showObj.deleteShow(full=self.full_delete) return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has been deleted") @@ -4139,7 +4251,7 @@ class CMD_SickGearShowUpdate(ApiCall): sickbeard.showQueueScheduler.action.updateShow(showObj, True) #@UndefinedVariable return _responds(RESULT_SUCCESS, msg=str(showObj.name) + " has queued to be updated") except exceptions.CantUpdateException as e: - logger.log(u"API:: Unable to update " + str(showObj.name) + ". " + str(ex(e)), logger.ERROR) + self.log(u"Unable to update " + str(showObj.name) + ". " + str(ex(e)), logger.ERROR) return _responds(RESULT_FAILURE, msg="Unable to update " + str(showObj.name)) @@ -4230,7 +4342,8 @@ class CMD_SickGearShows(ApiCall): else: showDict['next_ep_airdate'] = '' - showDict["cache"] = CMD_ShowCache(self.handler, (), {"indexerid": curShow.indexerid}).run()["data"] + showDict["cache"] = CMD_SickGearShowCache(self.handler, (), {"indexer": curShow.indexer, + "indexerid": curShow.indexerid}).run()["data"] if not showDict["network"]: showDict["network"] = "" if self.sort == "name": @@ -4459,12 +4572,14 @@ _functionMaper = {"help": CMD_Help, "sb.searchtvdb": CMD_SickBeardSearchIndexers, "sg.searchtv": CMD_SickGearSearchIndexers, "sb.setdefaults": CMD_SickBeardSetDefaults, + "sg.setdefaults": CMD_SickGearSetDefaults, "sg.setscenenumber": CMD_SickGearSetSceneNumber, "sg.activatescenenumbering": CMD_SickGearActivateSceneNumber, "sg.getindexers": CMD_SickGearGetIndexers, "sg.getindexericon": CMD_SickGearGetIndexerIcon, "sg.getnetworkicon": CMD_SickGearGetNetworkIcon, "sb.shutdown": CMD_SickBeardShutdown, + "sg.shutdown": CMD_SickGearShutdown, "sg.listignorewords": CMD_SickGearListIgnoreWords, "sg.setignorewords": CMD_SickGearSetIgnoreWords, "sg.listrequiredwords": CMD_SickGearListRequireWords, @@ -4512,3 +4627,5 @@ _functionMaper = {"help": CMD_Help, "sg.shows.forceupdate": CMD_SickGearShowsForceUpdate, "sg.shows.queue": CMD_SickGearShowsQueue, } + +_functionMaper_reversed = {v: k for k, v in iteritems(_functionMaper)} diff --git a/sickbeard/webserve.py b/sickbeard/webserve.py index 71f4bf7..88f0630 100644 --- a/sickbeard/webserve.py +++ b/sickbeard/webserve.py @@ -5854,6 +5854,8 @@ class ConfigGeneral(Config): t.indexers = dict([(i, sickbeard.indexerApi().indexers[i]) for i in sickbeard.indexerApi().indexers if sickbeard.indexerApi(i).config['active']]) t.request_host = escape.xhtml_escape(self.request.host_name) + api_keys = '|||'.join([':::'.join(a) for a in sickbeard.API_KEYS]) + t.api_keys = api_keys and sickbeard.API_KEYS or [] return t.respond() def saveRootDirs(self, rootDirString=None): @@ -5891,7 +5893,8 @@ class ConfigGeneral(Config): sickbeard.save_config() - def generateKey(self, *args, **kwargs): + @staticmethod + def generateKey(*args, **kwargs): """ Return a new randomized API_KEY """ @@ -5911,20 +5914,60 @@ class ConfigGeneral(Config): m.update(r) # Return a hex digest of the md5, eg 49f68a5c8493ec2c0bf489821c21fc3b - logger.log(u'New API generated') + app_name = kwargs.get('app_name') + app_name = '' if not app_name else ' for [%s]' % app_name + logger.log(u'New apikey generated%s' % app_name) return m.hexdigest() + def create_apikey(self, app_name): + result = dict() + if not app_name: + result['result'] = 'Failed: no name given' + elif app_name in [k[0] for k in sickbeard.API_KEYS if k[0]]: + result['result'] = 'Failed: name is not unique' + else: + api_key = self.generateKey(app_name=app_name) + if api_key in [k[1] for k in sickbeard.API_KEYS if k[0]]: + result['result'] = 'Failed: apikey already exists, try again' + else: + sickbeard.API_KEYS.append([app_name, api_key]) + logger.log('Created apikey for [%s]' % app_name, logger.DEBUG) + result.update(dict(result='Success: apikey added', added=api_key)) + sickbeard.USE_API = 1 + sickbeard.save_config() + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) + + return json.dumps(result) + + @staticmethod + def revoke_apikey(app_name, api_key): + result = dict() + if not app_name: + result['result'] = 'Failed: no name given' + elif not api_key or 32 != len(re.sub('(?i)[^0-9a-f]', '', api_key)): + result['result'] = 'Failed: key not valid' + elif api_key not in [k[1] for k in sickbeard.API_KEYS if k[0]]: + result['result'] = 'Failed: key doesn\'t exist' + else: + sickbeard.API_KEYS = [ak for ak in sickbeard.API_KEYS if ak[0] and api_key != ak[1]] + logger.log('Revoked [%s] apikey [%s]' % (app_name, api_key), logger.DEBUG) + result.update(dict(result='Success: apikey removed', removed=True)) + sickbeard.save_config() + ui.notifications.message('Configuration Saved', ek.ek(os.path.join, sickbeard.CONFIG_FILE)) + + return json.dumps(result) + def saveGeneral(self, log_dir=None, web_port=None, web_log=None, encryption_version=None, web_ipv6=None, web_ipv64=None, update_shows_on_start=None, show_update_hour=None, trash_remove_show=None, trash_rotate_logs=None, update_frequency=None, launch_browser=None, web_username=None, - use_api=None, api_key=None, indexer_default=None, timezone_display=None, cpu_preset=None, file_logging_preset=None, + use_api=None, indexer_default=None, timezone_display=None, cpu_preset=None, file_logging_preset=None, web_password=None, version_notify=None, enable_https=None, https_cert=None, https_key=None, handle_reverse_proxy=None, send_security_headers=None, allowed_hosts=None, home_search_focus=None, display_freespace=None, sort_article=None, auto_update=None, notify_on_update=None, proxy_setting=None, proxy_indexers=None, anon_redirect=None, git_path=None, git_remote=None, calendar_unprotected=None, fuzzy_dating=None, trim_zero=None, date_preset=None, date_preset_na=None, time_preset=None, indexer_timeout=None, rootDir=None, show_dirs_with_dots=None, theme_name=None, default_home=None, - use_imdb_info=None, fanart_limit=None, show_tags=None, showlist_tagview=None): + use_imdb_info=None, fanart_limit=None, show_tags=None, showlist_tagview=None, **kwargs): results = [] @@ -6000,7 +6043,6 @@ class ConfigGeneral(Config): sickbeard.CALENDAR_UNPROTECTED = config.checkbox_to_value(calendar_unprotected) sickbeard.USE_API = config.checkbox_to_value(use_api) - sickbeard.API_KEY = api_key sickbeard.WEB_PORT = config.to_int(web_port) # sickbeard.WEB_LOG is set in config.change_log_dir() @@ -7313,8 +7355,9 @@ class ApiBuilder(MainHandler): t.indexers = sickbeard.indexerApi().all_indexers t.searchindexers = sickbeard.indexerApi().search_indexers - if len(sickbeard.API_KEY) == 32: - t.apikey = sickbeard.API_KEY + if len(sickbeard.API_KEYS): + # use first APIKEY for apibuilder tests + t.apikey = sickbeard.API_KEYS[0][1] else: t.apikey = 'api key not generated'