From 37df262b24f7ed741117ef78ffad1d610e7816cd Mon Sep 17 00:00:00 2001 From: John Date: Mon, 28 Sep 2015 03:04:15 +0200 Subject: [PATCH 01/24] Good day plex! --- .../default/css/selectize.bootstrap3.css | 401 ++++++++++++++++++ data/interfaces/default/js/selectize.min.js | 3 + data/interfaces/default/welcome.html | 163 +++++-- plexpy/plextv.py | 25 +- plexpy/webserve.py | 39 +- 5 files changed, 578 insertions(+), 53 deletions(-) create mode 100644 data/interfaces/default/css/selectize.bootstrap3.css create mode 100644 data/interfaces/default/js/selectize.min.js diff --git a/data/interfaces/default/css/selectize.bootstrap3.css b/data/interfaces/default/css/selectize.bootstrap3.css new file mode 100644 index 00000000..cfb2bfa2 --- /dev/null +++ b/data/interfaces/default/css/selectize.bootstrap3.css @@ -0,0 +1,401 @@ +/** + * selectize.bootstrap3.css (v0.12.1) - Bootstrap 3 Theme + * Copyright (c) 2013–2015 Brian Reavis & contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this + * file except in compliance with the License. You may obtain a copy of the License at: + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF + * ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + * + * @author Brian Reavis + */ +.selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { + visibility: visible !important; + background: #f2f2f2 !important; + background: rgba(0, 0, 0, 0.06) !important; + border: 0 none !important; + -webkit-box-shadow: inset 0 0 12px 4px #ffffff; + box-shadow: inset 0 0 12px 4px #ffffff; +} +.selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { + content: '!'; + visibility: hidden; +} +.selectize-control.plugin-drag_drop .ui-sortable-helper { + -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); + box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); +} +.selectize-dropdown-header { + position: relative; + padding: 3px 12px; + border-bottom: 1px solid #d0d0d0; + background: #f8f8f8; + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.selectize-dropdown-header-close { + position: absolute; + right: 12px; + top: 50%; + color: #333333; + opacity: 0.4; + margin-top: -12px; + line-height: 20px; + font-size: 20px !important; +} +.selectize-dropdown-header-close:hover { + color: #000000; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup { + border-right: 1px solid #f2f2f2; + border-top: 0 none; + float: left; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { + border-right: 0 none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup:before { + display: none; +} +.selectize-dropdown.plugin-optgroup_columns .optgroup-header { + border-top: 0 none; +} +.selectize-control.plugin-remove_button [data-value] { + position: relative; + padding-right: 24px !important; +} +.selectize-control.plugin-remove_button [data-value] .remove { + z-index: 1; + /* fixes ie bug (see #392) */ + position: absolute; + top: 0; + right: 0; + bottom: 0; + width: 17px; + text-align: center; + font-weight: bold; + font-size: 12px; + color: inherit; + text-decoration: none; + vertical-align: middle; + display: inline-block; + padding: 1px 0 0 0; + border-left: 1px solid rgba(0, 0, 0, 0); + -webkit-border-radius: 0 2px 2px 0; + -moz-border-radius: 0 2px 2px 0; + border-radius: 0 2px 2px 0; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.selectize-control.plugin-remove_button [data-value] .remove:hover { + background: rgba(0, 0, 0, 0.05); +} +.selectize-control.plugin-remove_button [data-value].active .remove { + border-left-color: rgba(0, 0, 0, 0); +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { + background: none; +} +.selectize-control.plugin-remove_button .disabled [data-value] .remove { + border-left-color: rgba(77, 77, 77, 0); +} +.selectize-control { + position: relative; +} +.selectize-dropdown, +.selectize-input, +.selectize-input input { + color: #333333; + font-family: inherit; + font-size: inherit; + line-height: 20px; + -webkit-font-smoothing: inherit; +} +.selectize-input, +.selectize-control.single .selectize-input.input-active { + background: #ffffff; + cursor: text; + display: inline-block; +} +.selectize-input { + border: 1px solid #cccccc; + padding: 6px 12px; + display: inline-block; + width: 100%; + overflow: hidden; + position: relative; + z-index: 1; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.selectize-control.multi .selectize-input.has-items { + padding: 5px 12px 2px; +} +.selectize-input.full { + background-color: #ffffff; +} +.selectize-input.disabled, +.selectize-input.disabled * { + cursor: default !important; +} +.selectize-input.focus { + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); +} +.selectize-input.dropdown-active { + -webkit-border-radius: 4px 4px 0 0; + -moz-border-radius: 4px 4px 0 0; + border-radius: 4px 4px 0 0; +} +.selectize-input > * { + vertical-align: baseline; + display: -moz-inline-stack; + display: inline-block; + zoom: 1; + *display: inline; +} +.selectize-control.multi .selectize-input > div { + cursor: pointer; + margin: 0 3px 3px 0; + padding: 1px 3px; + background: #efefef; + color: #333333; + border: 0 solid rgba(0, 0, 0, 0); +} +.selectize-control.multi .selectize-input > div.active { + background: #428bca; + color: #ffffff; + border: 0 solid rgba(0, 0, 0, 0); +} +.selectize-control.multi .selectize-input.disabled > div, +.selectize-control.multi .selectize-input.disabled > div.active { + color: #808080; + background: #ffffff; + border: 0 solid rgba(77, 77, 77, 0); +} +.selectize-input > input { + display: inline-block !important; + padding: 0 !important; + min-height: 0 !important; + max-height: none !important; + max-width: 100% !important; + margin: 0 !important; + text-indent: 0 !important; + border: 0 none !important; + background: none !important; + line-height: inherit !important; + -webkit-user-select: auto !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.selectize-input > input::-ms-clear { + display: none; +} +.selectize-input > input:focus { + outline: none !important; +} +.selectize-input::after { + content: ' '; + display: block; + clear: left; +} +.selectize-input.dropdown-active::before { + content: ' '; + display: block; + position: absolute; + background: #ffffff; + height: 1px; + bottom: 0; + left: 0; + right: 0; +} +.selectize-dropdown { + position: absolute; + z-index: 10; + border: 1px solid #d0d0d0; + background: #ffffff; + margin: -1px 0 0 0; + border-top: 0 none; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + -webkit-border-radius: 0 0 4px 4px; + -moz-border-radius: 0 0 4px 4px; + border-radius: 0 0 4px 4px; +} +.selectize-dropdown [data-selectable] { + cursor: pointer; + overflow: hidden; +} +.selectize-dropdown [data-selectable] .highlight { + background: rgba(255, 237, 40, 0.4); + -webkit-border-radius: 1px; + -moz-border-radius: 1px; + border-radius: 1px; +} +.selectize-dropdown [data-selectable], +.selectize-dropdown .optgroup-header { + padding: 3px 12px; +} +.selectize-dropdown .optgroup:first-child .optgroup-header { + border-top: 0 none; +} +.selectize-dropdown .optgroup-header { + color: #777777; + background: #ffffff; + cursor: default; +} +.selectize-dropdown .active { + background-color: #f5f5f5; + color: #262626; +} +.selectize-dropdown .active.create { + color: #262626; +} +.selectize-dropdown .create { + color: rgba(51, 51, 51, 0.5); +} +.selectize-dropdown-content { + overflow-y: auto; + overflow-x: hidden; + max-height: 200px; +} +.selectize-control.single .selectize-input, +.selectize-control.single .selectize-input input { + cursor: pointer; +} +.selectize-control.single .selectize-input.input-active, +.selectize-control.single .selectize-input.input-active input { + cursor: text; +} +.selectize-control.single .selectize-input:after { + content: ' '; + display: block; + position: absolute; + top: 50%; + right: 17px; + margin-top: -3px; + width: 0; + height: 0; + border-style: solid; + border-width: 5px 5px 0 5px; + border-color: #333333 transparent transparent transparent; +} +.selectize-control.single .selectize-input.dropdown-active:after { + margin-top: -4px; + border-width: 0 5px 5px 5px; + border-color: transparent transparent #333333 transparent; +} +.selectize-control.rtl.single .selectize-input:after { + left: 17px; + right: auto; +} +.selectize-control.rtl .selectize-input > input { + margin: 0 4px 0 -2px !important; +} +.selectize-control .selectize-input.disabled { + opacity: 0.5; + background-color: #ffffff; +} +.selectize-dropdown, +.selectize-dropdown.form-control { + height: auto; + padding: 0; + margin: 2px 0 0 0; + z-index: 1000; + background: #ffffff; + border: 1px solid #cccccc; + border: 1px solid rgba(0, 0, 0, 0.15); + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); +} +.selectize-dropdown .optgroup-header { + font-size: 12px; + line-height: 1.42857143; +} +.selectize-dropdown .optgroup:first-child:before { + display: none; +} +.selectize-dropdown .optgroup:before { + content: ' '; + display: block; + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; + margin-left: -12px; + margin-right: -12px; +} +.selectize-dropdown-content { + padding: 5px 0; +} +.selectize-dropdown-header { + padding: 6px 12px; +} +.selectize-input { + min-height: 34px; +} +.selectize-input.dropdown-active { + -webkit-border-radius: 4px; + -moz-border-radius: 4px; + border-radius: 4px; +} +.selectize-input.dropdown-active::before { + display: none; +} +.selectize-input.focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6); +} +.has-error .selectize-input { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} +.has-error .selectize-input:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; +} +.selectize-control.multi .selectize-input.has-items { + padding-left: 9px; + padding-right: 9px; +} +.selectize-control.multi .selectize-input > div { + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +.form-control.selectize-control { + padding: 0; + height: auto; + border: none; + background: none; + -webkit-box-shadow: none; + box-shadow: none; + -webkit-border-radius: 0; + -moz-border-radius: 0; + border-radius: 0; +} diff --git a/data/interfaces/default/js/selectize.min.js b/data/interfaces/default/js/selectize.min.js new file mode 100644 index 00000000..281aebb6 --- /dev/null +++ b/data/interfaces/default/js/selectize.min.js @@ -0,0 +1,3 @@ +/*! selectize.js - v0.12.1 | https://github.com/brianreavis/selectize.js | Apache License (v2) */ +!function(a,b){"function"==typeof define&&define.amd?define("sifter",b):"object"==typeof exports?module.exports=b():a.Sifter=b()}(this,function(){var a=function(a,b){this.items=a,this.settings=b||{diacritics:!0}};a.prototype.tokenize=function(a){if(a=d(String(a||"").toLowerCase()),!a||!a.length)return[];var b,c,f,h,i=[],j=a.split(/ +/);for(b=0,c=j.length;c>b;b++){if(f=e(j[b]),this.settings.diacritics)for(h in g)g.hasOwnProperty(h)&&(f=f.replace(new RegExp(h,"g"),g[h]));i.push({string:j[b],regex:new RegExp(f,"i")})}return i},a.prototype.iterator=function(a,b){var c;c=f(a)?Array.prototype.forEach||function(a){for(var b=0,c=this.length;c>b;b++)a(this[b],b,this)}:function(a){for(var b in this)this.hasOwnProperty(b)&&a(this[b],b,this)},c.apply(a,[b])},a.prototype.getScoreFunction=function(a,b){var c,d,e,f;c=this,a=c.prepareSearch(a,b),e=a.tokens,d=a.options.fields,f=e.length;var g=function(a,b){var c,d;return a?(a=String(a||""),d=a.search(b.regex),-1===d?0:(c=b.string.length/a.length,0===d&&(c+=.5),c)):0},h=function(){var a=d.length;return a?1===a?function(a,b){return g(b[d[0]],a)}:function(b,c){for(var e=0,f=0;a>e;e++)f+=g(c[d[e]],b);return f/a}:function(){return 0}}();return f?1===f?function(a){return h(e[0],a)}:"and"===a.options.conjunction?function(a){for(var b,c=0,d=0;f>c;c++){if(b=h(e[c],a),0>=b)return 0;d+=b}return d/f}:function(a){for(var b=0,c=0;f>b;b++)c+=h(e[b],a);return c/f}:function(){return 0}},a.prototype.getSortFunction=function(a,c){var d,e,f,g,h,i,j,k,l,m,n;if(f=this,a=f.prepareSearch(a,c),n=!a.query&&c.sort_empty||c.sort,l=function(a,b){return"$score"===a?b.score:f.items[b.id][a]},h=[],n)for(d=0,e=n.length;e>d;d++)(a.query||"$score"!==n[d].field)&&h.push(n[d]);if(a.query){for(m=!0,d=0,e=h.length;e>d;d++)if("$score"===h[d].field){m=!1;break}m&&h.unshift({field:"$score",direction:"desc"})}else for(d=0,e=h.length;e>d;d++)if("$score"===h[d].field){h.splice(d,1);break}for(k=[],d=0,e=h.length;e>d;d++)k.push("desc"===h[d].direction?-1:1);return i=h.length,i?1===i?(g=h[0].field,j=k[0],function(a,c){return j*b(l(g,a),l(g,c))}):function(a,c){var d,e,f;for(d=0;i>d;d++)if(f=h[d].field,e=k[d]*b(l(f,a),l(f,c)))return e;return 0}:null},a.prototype.prepareSearch=function(a,b){if("object"==typeof a)return a;b=c({},b);var d=b.fields,e=b.sort,g=b.sort_empty;return d&&!f(d)&&(b.fields=[d]),e&&!f(e)&&(b.sort=[e]),g&&!f(g)&&(b.sort_empty=[g]),{options:b,query:String(a||"").toLowerCase(),tokens:this.tokenize(a),total:0,items:[]}},a.prototype.search=function(a,b){var c,d,e,f,g=this;return d=this.prepareSearch(a,b),b=d.options,a=d.query,f=b.score||g.getScoreFunction(d),a.length?g.iterator(g.items,function(a,e){c=f(a),(b.filter===!1||c>0)&&d.items.push({score:c,id:e})}):g.iterator(g.items,function(a,b){d.items.push({score:1,id:b})}),e=g.getSortFunction(d,b),e&&d.items.sort(e),d.total=d.items.length,"number"==typeof b.limit&&(d.items=d.items.slice(0,b.limit)),d};var b=function(a,b){return"number"==typeof a&&"number"==typeof b?a>b?1:b>a?-1:0:(a=h(String(a||"")),b=h(String(b||"")),a>b?1:b>a?-1:0)},c=function(a){var b,c,d,e;for(b=1,c=arguments.length;c>b;b++)if(e=arguments[b])for(d in e)e.hasOwnProperty(d)&&(a[d]=e[d]);return a},d=function(a){return(a+"").replace(/^\s+|\s+$|/g,"")},e=function(a){return(a+"").replace(/([.?*+^$[\]\\(){}|-])/g,"\\$1")},f=Array.isArray||$&&$.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)},g={a:"[aÀÁÂÃÄÅàáâãäåĀāąĄ]",c:"[cÇçćĆčČ]",d:"[dđĐďĎ]",e:"[eÈÉÊËèéêëěĚĒēęĘ]",i:"[iÌÍÎÏìíîïĪī]",l:"[lłŁ]",n:"[nÑñňŇńŃ]",o:"[oÒÓÔÕÕÖØòóôõöøŌō]",r:"[rřŘ]",s:"[sŠšśŚ]",t:"[tťŤ]",u:"[uÙÚÛÜùúûüůŮŪū]",y:"[yŸÿýÝ]",z:"[zŽžżŻźŹ]"},h=function(){var a,b,c,d,e="",f={};for(c in g)if(g.hasOwnProperty(c))for(d=g[c].substring(2,g[c].length-1),e+=d,a=0,b=d.length;b>a;a++)f[d.charAt(a)]=c;var h=new RegExp("["+e+"]","g");return function(a){return a.replace(h,function(a){return f[a]}).toLowerCase()}}();return a}),function(a,b){"function"==typeof define&&define.amd?define("microplugin",b):"object"==typeof exports?module.exports=b():a.MicroPlugin=b()}(this,function(){var a={};a.mixin=function(a){a.plugins={},a.prototype.initializePlugins=function(a){var c,d,e,f=this,g=[];if(f.plugins={names:[],settings:{},requested:{},loaded:{}},b.isArray(a))for(c=0,d=a.length;d>c;c++)"string"==typeof a[c]?g.push(a[c]):(f.plugins.settings[a[c].name]=a[c].options,g.push(a[c].name));else if(a)for(e in a)a.hasOwnProperty(e)&&(f.plugins.settings[e]=a[e],g.push(e));for(;g.length;)f.require(g.shift())},a.prototype.loadPlugin=function(b){var c=this,d=c.plugins,e=a.plugins[b];if(!a.plugins.hasOwnProperty(b))throw new Error('Unable to find "'+b+'" plugin');d.requested[b]=!0,d.loaded[b]=e.fn.apply(c,[c.plugins.settings[b]||{}]),d.names.push(b)},a.prototype.require=function(a){var b=this,c=b.plugins;if(!b.plugins.loaded.hasOwnProperty(a)){if(c.requested[a])throw new Error('Plugin has circular dependency ("'+a+'")');b.loadPlugin(a)}return c.loaded[a]},a.define=function(b,c){a.plugins[b]={name:b,fn:c}}};var b={isArray:Array.isArray||function(a){return"[object Array]"===Object.prototype.toString.call(a)}};return a}),function(a,b){"function"==typeof define&&define.amd?define("selectize",["jquery","sifter","microplugin"],b):"object"==typeof exports?module.exports=b(require("jquery"),require("sifter"),require("microplugin")):a.Selectize=b(a.jQuery,a.Sifter,a.MicroPlugin)}(this,function(a,b,c){"use strict";var d=function(a,b){if("string"!=typeof b||b.length){var c="string"==typeof b?new RegExp(b,"i"):b,d=function(a){var b=0;if(3===a.nodeType){var e=a.data.search(c);if(e>=0&&a.data.length>0){var f=a.data.match(c),g=document.createElement("span");g.className="highlight";var h=a.splitText(e),i=(h.splitText(f[0].length),h.cloneNode(!0));g.appendChild(i),h.parentNode.replaceChild(g,h),b=1}}else if(1===a.nodeType&&a.childNodes&&!/(script|style)/i.test(a.tagName))for(var j=0;j/g,">").replace(/"/g,""")},B=function(a){return(a+"").replace(/\$/g,"$$$$")},C={};C.before=function(a,b,c){var d=a[b];a[b]=function(){return c.apply(a,arguments),d.apply(a,arguments)}},C.after=function(a,b,c){var d=a[b];a[b]=function(){var b=d.apply(a,arguments);return c.apply(a,arguments),b}};var D=function(a){var b=!1;return function(){b||(b=!0,a.apply(this,arguments))}},E=function(a,b){var c;return function(){var d=this,e=arguments;window.clearTimeout(c),c=window.setTimeout(function(){a.apply(d,e)},b)}},F=function(a,b,c){var d,e=a.trigger,f={};a.trigger=function(){var c=arguments[0];return-1===b.indexOf(c)?e.apply(a,arguments):void(f[c]=arguments)},c.apply(a,[]),a.trigger=e;for(d in f)f.hasOwnProperty(d)&&e.apply(a,f[d])},G=function(a,b,c,d){a.on(b,c,function(b){for(var c=b.target;c&&c.parentNode!==a[0];)c=c.parentNode;return b.currentTarget=c,d.apply(this,[b])})},H=function(a){var b={};if("selectionStart"in a)b.start=a.selectionStart,b.length=a.selectionEnd-b.start;else if(document.selection){a.focus();var c=document.selection.createRange(),d=document.selection.createRange().text.length;c.moveStart("character",-a.value.length),b.start=c.text.length-d,b.length=d}return b},I=function(a,b,c){var d,e,f={};if(c)for(d=0,e=c.length;e>d;d++)f[c[d]]=a.css(c[d]);else f=a.css();b.css(f)},J=function(b,c){if(!b)return 0;var d=a("").css({position:"absolute",top:-99999,left:-99999,width:"auto",padding:0,whiteSpace:"pre"}).text(b).appendTo("body");I(c,d,["letterSpacing","fontSize","fontFamily","fontWeight","textTransform"]);var e=d.width();return d.remove(),e},K=function(a){var b=null,c=function(c,d){var e,f,g,h,i,j,k,l;c=c||window.event||{},d=d||{},c.metaKey||c.altKey||(d.force||a.data("grow")!==!1)&&(e=a.val(),c.type&&"keydown"===c.type.toLowerCase()&&(f=c.keyCode,g=f>=97&&122>=f||f>=65&&90>=f||f>=48&&57>=f||32===f,f===q||f===p?(l=H(a[0]),l.length?e=e.substring(0,l.start)+e.substring(l.start+l.length):f===p&&l.start?e=e.substring(0,l.start-1)+e.substring(l.start+1):f===q&&"undefined"!=typeof l.start&&(e=e.substring(0,l.start)+e.substring(l.start+1))):g&&(j=c.shiftKey,k=String.fromCharCode(c.keyCode),k=j?k.toUpperCase():k.toLowerCase(),e+=k)),h=a.attr("placeholder"),!e&&h&&(e=h),i=J(e,a)+4,i!==b&&(b=i,a.width(i),a.triggerHandler("resize")))};a.on("keydown keyup update blur",c),c()},L=function(c,d){var e,f,g,h,i=this;h=c[0],h.selectize=i;var j=window.getComputedStyle&&window.getComputedStyle(h,null);if(g=j?j.getPropertyValue("direction"):h.currentStyle&&h.currentStyle.direction,g=g||c.parents("[dir]:first").attr("dir")||"",a.extend(i,{order:0,settings:d,$input:c,tabIndex:c.attr("tabindex")||"",tagType:"select"===h.tagName.toLowerCase()?v:w,rtl:/rtl/i.test(g),eventNS:".selectize"+ ++L.count,highlightedValue:null,isOpen:!1,isDisabled:!1,isRequired:c.is("[required]"),isInvalid:!1,isLocked:!1,isFocused:!1,isInputHidden:!1,isSetup:!1,isShiftDown:!1,isCmdDown:!1,isCtrlDown:!1,ignoreFocus:!1,ignoreBlur:!1,ignoreHover:!1,hasOptions:!1,currentResults:null,lastValue:"",caretPos:0,loading:0,loadedSearches:{},$activeOption:null,$activeItems:[],optgroups:{},options:{},userOptions:{},items:[],renderCache:{},onSearchChange:null===d.loadThrottle?i.onSearchChange:E(i.onSearchChange,d.loadThrottle)}),i.sifter=new b(this.options,{diacritics:d.diacritics}),i.settings.options){for(e=0,f=i.settings.options.length;f>e;e++)i.registerOption(i.settings.options[e]);delete i.settings.options}if(i.settings.optgroups){for(e=0,f=i.settings.optgroups.length;f>e;e++)i.registerOptionGroup(i.settings.optgroups[e]);delete i.settings.optgroups}i.settings.mode=i.settings.mode||(1===i.settings.maxItems?"single":"multi"),"boolean"!=typeof i.settings.hideSelected&&(i.settings.hideSelected="multi"===i.settings.mode),i.initializePlugins(i.settings.plugins),i.setupCallbacks(),i.setupTemplates(),i.setup()};return e.mixin(L),c.mixin(L),a.extend(L.prototype,{setup:function(){var b,c,d,e,g,h,i,j,k,l=this,m=l.settings,n=l.eventNS,o=a(window),p=a(document),q=l.$input;if(i=l.settings.mode,j=q.attr("class")||"",b=a("
").addClass(m.wrapperClass).addClass(j).addClass(i),c=a("
").addClass(m.inputClass).addClass("items").appendTo(b),d=a('').appendTo(c).attr("tabindex",q.is(":disabled")?"-1":l.tabIndex),h=a(m.dropdownParent||b),e=a("
").addClass(m.dropdownClass).addClass(i).hide().appendTo(h),g=a("
").addClass(m.dropdownContentClass).appendTo(e),l.settings.copyClassesToDropdown&&e.addClass(j),b.css({width:q[0].style.width}),l.plugins.names.length&&(k="plugin-"+l.plugins.names.join(" plugin-"),b.addClass(k),e.addClass(k)),(null===m.maxItems||m.maxItems>1)&&l.tagType===v&&q.attr("multiple","multiple"),l.settings.placeholder&&d.attr("placeholder",m.placeholder),!l.settings.splitOn&&l.settings.delimiter){var u=l.settings.delimiter.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&");l.settings.splitOn=new RegExp("\\s*"+u+"+\\s*")}q.attr("autocorrect")&&d.attr("autocorrect",q.attr("autocorrect")),q.attr("autocapitalize")&&d.attr("autocapitalize",q.attr("autocapitalize")),l.$wrapper=b,l.$control=c,l.$control_input=d,l.$dropdown=e,l.$dropdown_content=g,e.on("mouseenter","[data-selectable]",function(){return l.onOptionHover.apply(l,arguments)}),e.on("mousedown click","[data-selectable]",function(){return l.onOptionSelect.apply(l,arguments)}),G(c,"mousedown","*:not(input)",function(){return l.onItemSelect.apply(l,arguments)}),K(d),c.on({mousedown:function(){return l.onMouseDown.apply(l,arguments)},click:function(){return l.onClick.apply(l,arguments)}}),d.on({mousedown:function(a){a.stopPropagation()},keydown:function(){return l.onKeyDown.apply(l,arguments)},keyup:function(){return l.onKeyUp.apply(l,arguments)},keypress:function(){return l.onKeyPress.apply(l,arguments)},resize:function(){l.positionDropdown.apply(l,[])},blur:function(){return l.onBlur.apply(l,arguments)},focus:function(){return l.ignoreBlur=!1,l.onFocus.apply(l,arguments)},paste:function(){return l.onPaste.apply(l,arguments)}}),p.on("keydown"+n,function(a){l.isCmdDown=a[f?"metaKey":"ctrlKey"],l.isCtrlDown=a[f?"altKey":"ctrlKey"],l.isShiftDown=a.shiftKey}),p.on("keyup"+n,function(a){a.keyCode===t&&(l.isCtrlDown=!1),a.keyCode===r&&(l.isShiftDown=!1),a.keyCode===s&&(l.isCmdDown=!1)}),p.on("mousedown"+n,function(a){if(l.isFocused){if(a.target===l.$dropdown[0]||a.target.parentNode===l.$dropdown[0])return!1;l.$control.has(a.target).length||a.target===l.$control[0]||l.blur(a.target)}}),o.on(["scroll"+n,"resize"+n].join(" "),function(){l.isOpen&&l.positionDropdown.apply(l,arguments)}),o.on("mousemove"+n,function(){l.ignoreHover=!1}),this.revertSettings={$children:q.children().detach(),tabindex:q.attr("tabindex")},q.attr("tabindex",-1).hide().after(l.$wrapper),a.isArray(m.items)&&(l.setValue(m.items),delete m.items),x&&q.on("invalid"+n,function(a){a.preventDefault(),l.isInvalid=!0,l.refreshState()}),l.updateOriginalInput(),l.refreshItems(),l.refreshState(),l.updatePlaceholder(),l.isSetup=!0,q.is(":disabled")&&l.disable(),l.on("change",this.onChange),q.data("selectize",l),q.addClass("selectized"),l.trigger("initialize"),m.preload===!0&&l.onSearchChange("")},setupTemplates:function(){var b=this,c=b.settings.labelField,d=b.settings.optgroupLabelField,e={optgroup:function(a){return'
'+a.html+"
"},optgroup_header:function(a,b){return'
'+b(a[d])+"
"},option:function(a,b){return'
'+b(a[c])+"
"},item:function(a,b){return'
'+b(a[c])+"
"},option_create:function(a,b){return'
Add '+b(a.input)+"
"}};b.settings.render=a.extend({},e,b.settings.render)},setupCallbacks:function(){var a,b,c={initialize:"onInitialize",change:"onChange",item_add:"onItemAdd",item_remove:"onItemRemove",clear:"onClear",option_add:"onOptionAdd",option_remove:"onOptionRemove",option_clear:"onOptionClear",optgroup_add:"onOptionGroupAdd",optgroup_remove:"onOptionGroupRemove",optgroup_clear:"onOptionGroupClear",dropdown_open:"onDropdownOpen",dropdown_close:"onDropdownClose",type:"onType",load:"onLoad",focus:"onFocus",blur:"onBlur"};for(a in c)c.hasOwnProperty(a)&&(b=this.settings[c[a]],b&&this.on(a,b))},onClick:function(a){var b=this;b.isFocused||(b.focus(),a.preventDefault())},onMouseDown:function(b){{var c=this,d=b.isDefaultPrevented();a(b.target)}if(c.isFocused){if(b.target!==c.$control_input[0])return"single"===c.settings.mode?c.isOpen?c.close():c.open():d||c.setActiveItem(null),!1}else d||window.setTimeout(function(){c.focus()},0)},onChange:function(){this.$input.trigger("change")},onPaste:function(b){var c=this;c.isFull()||c.isInputHidden||c.isLocked?b.preventDefault():c.settings.splitOn&&setTimeout(function(){for(var b=a.trim(c.$control_input.val()||"").split(c.settings.splitOn),d=0,e=b.length;e>d;d++)c.createItem(b[d])},0)},onKeyPress:function(a){if(this.isLocked)return a&&a.preventDefault();var b=String.fromCharCode(a.keyCode||a.which);return this.settings.create&&"multi"===this.settings.mode&&b===this.settings.delimiter?(this.createItem(),a.preventDefault(),!1):void 0},onKeyDown:function(a){var b=(a.target===this.$control_input[0],this);if(b.isLocked)return void(a.keyCode!==u&&a.preventDefault());switch(a.keyCode){case g:if(b.isCmdDown)return void b.selectAll();break;case i:return void(b.isOpen&&(a.preventDefault(),a.stopPropagation(),b.close()));case o:if(!a.ctrlKey||a.altKey)break;case n:if(!b.isOpen&&b.hasOptions)b.open();else if(b.$activeOption){b.ignoreHover=!0;var c=b.getAdjacentOption(b.$activeOption,1);c.length&&b.setActiveOption(c,!0,!0)}return void a.preventDefault();case l:if(!a.ctrlKey||a.altKey)break;case k:if(b.$activeOption){b.ignoreHover=!0;var d=b.getAdjacentOption(b.$activeOption,-1);d.length&&b.setActiveOption(d,!0,!0)}return void a.preventDefault();case h:return void(b.isOpen&&b.$activeOption&&(b.onOptionSelect({currentTarget:b.$activeOption}),a.preventDefault()));case j:return void b.advanceSelection(-1,a);case m:return void b.advanceSelection(1,a);case u:return b.settings.selectOnTab&&b.isOpen&&b.$activeOption&&(b.onOptionSelect({currentTarget:b.$activeOption}),b.isFull()||a.preventDefault()),void(b.settings.create&&b.createItem()&&a.preventDefault());case p:case q:return void b.deleteSelection(a)}return!b.isFull()&&!b.isInputHidden||(f?a.metaKey:a.ctrlKey)?void 0:void a.preventDefault()},onKeyUp:function(a){var b=this;if(b.isLocked)return a&&a.preventDefault();var c=b.$control_input.val()||"";b.lastValue!==c&&(b.lastValue=c,b.onSearchChange(c),b.refreshOptions(),b.trigger("type",c))},onSearchChange:function(a){var b=this,c=b.settings.load;c&&(b.loadedSearches.hasOwnProperty(a)||(b.loadedSearches[a]=!0,b.load(function(d){c.apply(b,[a,d])})))},onFocus:function(a){var b=this,c=b.isFocused;return b.isDisabled?(b.blur(),a&&a.preventDefault(),!1):void(b.ignoreFocus||(b.isFocused=!0,"focus"===b.settings.preload&&b.onSearchChange(""),c||b.trigger("focus"),b.$activeItems.length||(b.showInput(),b.setActiveItem(null),b.refreshOptions(!!b.settings.openOnFocus)),b.refreshState()))},onBlur:function(a,b){var c=this;if(c.isFocused&&(c.isFocused=!1,!c.ignoreFocus)){if(!c.ignoreBlur&&document.activeElement===c.$dropdown_content[0])return c.ignoreBlur=!0,void c.onFocus(a);var d=function(){c.close(),c.setTextboxValue(""),c.setActiveItem(null),c.setActiveOption(null),c.setCaret(c.items.length),c.refreshState(),(b||document.body).focus(),c.ignoreFocus=!1,c.trigger("blur")};c.ignoreFocus=!0,c.settings.create&&c.settings.createOnBlur?c.createItem(null,!1,d):d()}},onOptionHover:function(a){this.ignoreHover||this.setActiveOption(a.currentTarget,!1)},onOptionSelect:function(b){var c,d,e=this;b.preventDefault&&(b.preventDefault(),b.stopPropagation()),d=a(b.currentTarget),d.hasClass("create")?e.createItem(null,function(){e.settings.closeAfterSelect&&e.close()}):(c=d.attr("data-value"),"undefined"!=typeof c&&(e.lastQuery=null,e.setTextboxValue(""),e.addItem(c),e.settings.closeAfterSelect?e.close():!e.settings.hideSelected&&b.type&&/mouse/.test(b.type)&&e.setActiveOption(e.getOption(c))))},onItemSelect:function(a){var b=this;b.isLocked||"multi"===b.settings.mode&&(a.preventDefault(),b.setActiveItem(a.currentTarget,a))},load:function(a){var b=this,c=b.$wrapper.addClass(b.settings.loadingClass);b.loading++,a.apply(b,[function(a){b.loading=Math.max(b.loading-1,0),a&&a.length&&(b.addOption(a),b.refreshOptions(b.isFocused&&!b.isInputHidden)),b.loading||c.removeClass(b.settings.loadingClass),b.trigger("load",a)}])},setTextboxValue:function(a){var b=this.$control_input,c=b.val()!==a;c&&(b.val(a).triggerHandler("update"),this.lastValue=a)},getValue:function(){return this.tagType===v&&this.$input.attr("multiple")?this.items:this.items.join(this.settings.delimiter)},setValue:function(a,b){var c=b?[]:["change"];F(this,c,function(){this.clear(b),this.addItems(a,b)})},setActiveItem:function(b,c){var d,e,f,g,h,i,j,k,l=this;if("single"!==l.settings.mode){if(b=a(b),!b.length)return a(l.$activeItems).removeClass("active"),l.$activeItems=[],void(l.isFocused&&l.showInput());if(d=c&&c.type.toLowerCase(),"mousedown"===d&&l.isShiftDown&&l.$activeItems.length){for(k=l.$control.children(".active:last"),g=Array.prototype.indexOf.apply(l.$control[0].childNodes,[k[0]]),h=Array.prototype.indexOf.apply(l.$control[0].childNodes,[b[0]]),g>h&&(j=g,g=h,h=j),e=g;h>=e;e++)i=l.$control[0].childNodes[e],-1===l.$activeItems.indexOf(i)&&(a(i).addClass("active"),l.$activeItems.push(i));c.preventDefault()}else"mousedown"===d&&l.isCtrlDown||"keydown"===d&&this.isShiftDown?b.hasClass("active")?(f=l.$activeItems.indexOf(b[0]),l.$activeItems.splice(f,1),b.removeClass("active")):l.$activeItems.push(b.addClass("active")[0]):(a(l.$activeItems).removeClass("active"),l.$activeItems=[b.addClass("active")[0]]);l.hideInput(),this.isFocused||l.focus()}},setActiveOption:function(b,c,d){var e,f,g,h,i,j=this;j.$activeOption&&j.$activeOption.removeClass("active"),j.$activeOption=null,b=a(b),b.length&&(j.$activeOption=b.addClass("active"),(c||!y(c))&&(e=j.$dropdown_content.height(),f=j.$activeOption.outerHeight(!0),c=j.$dropdown_content.scrollTop()||0,g=j.$activeOption.offset().top-j.$dropdown_content.offset().top+c,h=g,i=g-e+f,g+f>e+c?j.$dropdown_content.stop().animate({scrollTop:i},d?j.settings.scrollDuration:0):c>g&&j.$dropdown_content.stop().animate({scrollTop:h},d?j.settings.scrollDuration:0)))},selectAll:function(){var a=this;"single"!==a.settings.mode&&(a.$activeItems=Array.prototype.slice.apply(a.$control.children(":not(input)").addClass("active")),a.$activeItems.length&&(a.hideInput(),a.close()),a.focus())},hideInput:function(){var a=this;a.setTextboxValue(""),a.$control_input.css({opacity:0,position:"absolute",left:a.rtl?1e4:-1e4}),a.isInputHidden=!0},showInput:function(){this.$control_input.css({opacity:1,position:"relative",left:0}),this.isInputHidden=!1},focus:function(){var a=this;a.isDisabled||(a.ignoreFocus=!0,a.$control_input[0].focus(),window.setTimeout(function(){a.ignoreFocus=!1,a.onFocus()},0))},blur:function(a){this.$control_input[0].blur(),this.onBlur(null,a)},getScoreFunction:function(a){return this.sifter.getScoreFunction(a,this.getSearchOptions())},getSearchOptions:function(){var a=this.settings,b=a.sortField;return"string"==typeof b&&(b=[{field:b}]),{fields:a.searchField,conjunction:a.searchConjunction,sort:b}},search:function(b){var c,d,e,f=this,g=f.settings,h=this.getSearchOptions();if(g.score&&(e=f.settings.score.apply(this,[b]),"function"!=typeof e))throw new Error('Selectize "score" setting must be a function that returns a function');if(b!==f.lastQuery?(f.lastQuery=b,d=f.sifter.search(b,a.extend(h,{score:e})),f.currentResults=d):d=a.extend(!0,{},f.currentResults),g.hideSelected)for(c=d.items.length-1;c>=0;c--)-1!==f.items.indexOf(z(d.items[c].id))&&d.items.splice(c,1);return d},refreshOptions:function(b){var c,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s;"undefined"==typeof b&&(b=!0);var t=this,u=a.trim(t.$control_input.val()),v=t.search(u),w=t.$dropdown_content,x=t.$activeOption&&z(t.$activeOption.attr("data-value"));for(g=v.items.length,"number"==typeof t.settings.maxOptions&&(g=Math.min(g,t.settings.maxOptions)),h={},i=[],c=0;g>c;c++)for(j=t.options[v.items[c].id],k=t.render("option",j),l=j[t.settings.optgroupField]||"",m=a.isArray(l)?l:[l],e=0,f=m&&m.length;f>e;e++)l=m[e],t.optgroups.hasOwnProperty(l)||(l=""),h.hasOwnProperty(l)||(h[l]=[],i.push(l)),h[l].push(k);for(this.settings.lockOptgroupOrder&&i.sort(function(a,b){var c=t.optgroups[a].$order||0,d=t.optgroups[b].$order||0;return c-d}),n=[],c=0,g=i.length;g>c;c++)l=i[c],t.optgroups.hasOwnProperty(l)&&h[l].length?(o=t.render("optgroup_header",t.optgroups[l])||"",o+=h[l].join(""),n.push(t.render("optgroup",a.extend({},t.optgroups[l],{html:o})))):n.push(h[l].join(""));if(w.html(n.join("")),t.settings.highlight&&v.query.length&&v.tokens.length)for(c=0,g=v.tokens.length;g>c;c++)d(w,v.tokens[c].regex);if(!t.settings.hideSelected)for(c=0,g=t.items.length;g>c;c++)t.getOption(t.items[c]).addClass("selected");p=t.canCreate(u),p&&(w.prepend(t.render("option_create",{input:u})),s=a(w[0].childNodes[0])),t.hasOptions=v.items.length>0||p,t.hasOptions?(v.items.length>0?(r=x&&t.getOption(x),r&&r.length?q=r:"single"===t.settings.mode&&t.items.length&&(q=t.getOption(t.items[0])),q&&q.length||(q=s&&!t.settings.addPrecedence?t.getAdjacentOption(s,1):w.find("[data-selectable]:first"))):q=s,t.setActiveOption(q),b&&!t.isOpen&&t.open()):(t.setActiveOption(null),b&&t.isOpen&&t.close())},addOption:function(b){var c,d,e,f=this;if(a.isArray(b))for(c=0,d=b.length;d>c;c++)f.addOption(b[c]);else(e=f.registerOption(b))&&(f.userOptions[e]=!0,f.lastQuery=null,f.trigger("option_add",e,b))},registerOption:function(a){var b=z(a[this.settings.valueField]);return!b||this.options.hasOwnProperty(b)?!1:(a.$order=a.$order||++this.order,this.options[b]=a,b)},registerOptionGroup:function(a){var b=z(a[this.settings.optgroupValueField]);return b?(a.$order=a.$order||++this.order,this.optgroups[b]=a,b):!1},addOptionGroup:function(a,b){b[this.settings.optgroupValueField]=a,(a=this.registerOptionGroup(b))&&this.trigger("optgroup_add",a,b)},removeOptionGroup:function(a){this.optgroups.hasOwnProperty(a)&&(delete this.optgroups[a],this.renderCache={},this.trigger("optgroup_remove",a))},clearOptionGroups:function(){this.optgroups={},this.renderCache={},this.trigger("optgroup_clear")},updateOption:function(b,c){var d,e,f,g,h,i,j,k=this;if(b=z(b),f=z(c[k.settings.valueField]),null!==b&&k.options.hasOwnProperty(b)){if("string"!=typeof f)throw new Error("Value must be set in option data");j=k.options[b].$order,f!==b&&(delete k.options[b],g=k.items.indexOf(b),-1!==g&&k.items.splice(g,1,f)),c.$order=c.$order||j,k.options[f]=c,h=k.renderCache.item,i=k.renderCache.option,h&&(delete h[b],delete h[f]),i&&(delete i[b],delete i[f]),-1!==k.items.indexOf(f)&&(d=k.getItem(b),e=a(k.render("item",c)),d.hasClass("active")&&e.addClass("active"),d.replaceWith(e)),k.lastQuery=null,k.isOpen&&k.refreshOptions(!1)}},removeOption:function(a,b){var c=this;a=z(a);var d=c.renderCache.item,e=c.renderCache.option;d&&delete d[a],e&&delete e[a],delete c.userOptions[a],delete c.options[a],c.lastQuery=null,c.trigger("option_remove",a),c.removeItem(a,b)},clearOptions:function(){var a=this;a.loadedSearches={},a.userOptions={},a.renderCache={},a.options=a.sifter.items={},a.lastQuery=null,a.trigger("option_clear"),a.clear()},getOption:function(a){return this.getElementWithValue(a,this.$dropdown_content.find("[data-selectable]"))},getAdjacentOption:function(b,c){var d=this.$dropdown.find("[data-selectable]"),e=d.index(b)+c;return e>=0&&ed;d++)if(c[d].getAttribute("data-value")===b)return a(c[d]);return a()},getItem:function(a){return this.getElementWithValue(a,this.$control.children())},addItems:function(b,c){for(var d=a.isArray(b)?b:[b],e=0,f=d.length;f>e;e++)this.isPending=f-1>e,this.addItem(d[e],c)},addItem:function(b,c){var d=c?[]:["change"];F(this,d,function(){var d,e,f,g,h,i=this,j=i.settings.mode;return b=z(b),-1!==i.items.indexOf(b)?void("single"===j&&i.close()):void(i.options.hasOwnProperty(b)&&("single"===j&&i.clear(c),"multi"===j&&i.isFull()||(d=a(i.render("item",i.options[b])),h=i.isFull(),i.items.splice(i.caretPos,0,b),i.insertAtCaret(d),(!i.isPending||!h&&i.isFull())&&i.refreshState(),i.isSetup&&(f=i.$dropdown_content.find("[data-selectable]"),i.isPending||(e=i.getOption(b),g=i.getAdjacentOption(e,1).attr("data-value"),i.refreshOptions(i.isFocused&&"single"!==j),g&&i.setActiveOption(i.getOption(g))),!f.length||i.isFull()?i.close():i.positionDropdown(),i.updatePlaceholder(),i.trigger("item_add",b,d),i.updateOriginalInput({silent:c})))))})},removeItem:function(a,b){var c,d,e,f=this;c="object"==typeof a?a:f.getItem(a),a=z(c.attr("data-value")),d=f.items.indexOf(a),-1!==d&&(c.remove(),c.hasClass("active")&&(e=f.$activeItems.indexOf(c[0]),f.$activeItems.splice(e,1)),f.items.splice(d,1),f.lastQuery=null,!f.settings.persist&&f.userOptions.hasOwnProperty(a)&&f.removeOption(a,b),d0),b.$control_input.data("grow",!c&&!d)},isFull:function(){return null!==this.settings.maxItems&&this.items.length>=this.settings.maxItems},updateOriginalInput:function(a){var b,c,d,e,f=this;if(a=a||{},f.tagType===v){for(d=[],b=0,c=f.items.length;c>b;b++)e=f.options[f.items[b]][f.settings.labelField]||"",d.push('");d.length||this.$input.attr("multiple")||d.push(''),f.$input.html(d.join(""))}else f.$input.val(f.getValue()),f.$input.attr("value",f.$input.val());f.isSetup&&(a.silent||f.trigger("change",f.$input.val()))},updatePlaceholder:function(){if(this.settings.placeholder){var a=this.$control_input;this.items.length?a.removeAttr("placeholder"):a.attr("placeholder",this.settings.placeholder),a.triggerHandler("update",{force:!0})}},open:function(){var a=this;a.isLocked||a.isOpen||"multi"===a.settings.mode&&a.isFull()||(a.focus(),a.isOpen=!0,a.refreshState(),a.$dropdown.css({visibility:"hidden",display:"block"}),a.positionDropdown(),a.$dropdown.css({visibility:"visible"}),a.trigger("dropdown_open",a.$dropdown))},close:function(){var a=this,b=a.isOpen;"single"===a.settings.mode&&a.items.length&&a.hideInput(),a.isOpen=!1,a.$dropdown.hide(),a.setActiveOption(null),a.refreshState(),b&&a.trigger("dropdown_close",a.$dropdown)},positionDropdown:function(){var a=this.$control,b="body"===this.settings.dropdownParent?a.offset():a.position();b.top+=a.outerHeight(!0),this.$dropdown.css({width:a.outerWidth(),top:b.top,left:b.left})},clear:function(a){var b=this;b.items.length&&(b.$control.children(":not(input)").remove(),b.items=[],b.lastQuery=null,b.setCaret(0),b.setActiveItem(null),b.updatePlaceholder(),b.updateOriginalInput({silent:a}),b.refreshState(),b.showInput(),b.trigger("clear"))},insertAtCaret:function(b){var c=Math.min(this.caretPos,this.items.length);0===c?this.$control.prepend(b):a(this.$control[0].childNodes[c]).before(b),this.setCaret(c+1)},deleteSelection:function(b){var c,d,e,f,g,h,i,j,k,l=this;if(e=b&&b.keyCode===p?-1:1,f=H(l.$control_input[0]),l.$activeOption&&!l.settings.hideSelected&&(i=l.getAdjacentOption(l.$activeOption,-1).attr("data-value")),g=[],l.$activeItems.length){for(k=l.$control.children(".active:"+(e>0?"last":"first")),h=l.$control.children(":not(input)").index(k),e>0&&h++,c=0,d=l.$activeItems.length;d>c;c++)g.push(a(l.$activeItems[c]).attr("data-value")); +b&&(b.preventDefault(),b.stopPropagation())}else(l.isFocused||"single"===l.settings.mode)&&l.items.length&&(0>e&&0===f.start&&0===f.length?g.push(l.items[l.caretPos-1]):e>0&&f.start===l.$control_input.val().length&&g.push(l.items[l.caretPos]));if(!g.length||"function"==typeof l.settings.onDelete&&l.settings.onDelete.apply(l,[g])===!1)return!1;for("undefined"!=typeof h&&l.setCaret(h);g.length;)l.removeItem(g.pop());return l.showInput(),l.positionDropdown(),l.refreshOptions(!0),i&&(j=l.getOption(i),j.length&&l.setActiveOption(j)),!0},advanceSelection:function(a,b){var c,d,e,f,g,h,i=this;0!==a&&(i.rtl&&(a*=-1),c=a>0?"last":"first",d=H(i.$control_input[0]),i.isFocused&&!i.isInputHidden?(f=i.$control_input.val().length,g=0>a?0===d.start&&0===d.length:d.start===f,g&&!f&&i.advanceCaret(a,b)):(h=i.$control.children(".active:"+c),h.length&&(e=i.$control.children(":not(input)").index(h),i.setActiveItem(null),i.setCaret(a>0?e+1:e))))},advanceCaret:function(a,b){var c,d,e=this;0!==a&&(c=a>0?"next":"prev",e.isShiftDown?(d=e.$control_input[c](),d.length&&(e.hideInput(),e.setActiveItem(d),b&&b.preventDefault())):e.setCaret(e.caretPos+a))},setCaret:function(b){var c=this;if(b="single"===c.settings.mode?c.items.length:Math.max(0,Math.min(c.items.length,b)),!c.isPending){var d,e,f,g;for(f=c.$control.children(":not(input)"),d=0,e=f.length;e>d;d++)g=a(f[d]).detach(),b>d?c.$control_input.before(g):c.$control.append(g)}c.caretPos=b},lock:function(){this.close(),this.isLocked=!0,this.refreshState()},unlock:function(){this.isLocked=!1,this.refreshState()},disable:function(){var a=this;a.$input.prop("disabled",!0),a.$control_input.prop("disabled",!0).prop("tabindex",-1),a.isDisabled=!0,a.lock()},enable:function(){var a=this;a.$input.prop("disabled",!1),a.$control_input.prop("disabled",!1).prop("tabindex",a.tabIndex),a.isDisabled=!1,a.unlock()},destroy:function(){var b=this,c=b.eventNS,d=b.revertSettings;b.trigger("destroy"),b.off(),b.$wrapper.remove(),b.$dropdown.remove(),b.$input.html("").append(d.$children).removeAttr("tabindex").removeClass("selectized").attr({tabindex:d.tabindex}).show(),b.$control_input.removeData("grow"),b.$input.removeData("selectize"),a(window).off(c),a(document).off(c),a(document.body).off(c),delete b.$input[0].selectize},render:function(a,b){var c,d,e="",f=!1,g=this,h=/^[\t \r\n]*<([a-z][a-z0-9\-_]*(?:\:[a-z][a-z0-9\-_]*)?)/i;return("option"===a||"item"===a)&&(c=z(b[g.settings.valueField]),f=!!c),f&&(y(g.renderCache[a])||(g.renderCache[a]={}),g.renderCache[a].hasOwnProperty(c))?g.renderCache[a][c]:(e=g.settings.render[a].apply(this,[b,A]),("option"===a||"option_create"===a)&&(e=e.replace(h,"<$1 data-selectable")),"optgroup"===a&&(d=b[g.settings.optgroupValueField]||"",e=e.replace(h,'<$1 data-group="'+B(A(d))+'"')),("option"===a||"item"===a)&&(e=e.replace(h,'<$1 data-value="'+B(A(c||""))+'"')),f&&(g.renderCache[a][c]=e),e)},clearCache:function(a){var b=this;"undefined"==typeof a?b.renderCache={}:delete b.renderCache[a]},canCreate:function(a){var b=this;if(!b.settings.create)return!1;var c=b.settings.createFilter;return!(!a.length||"function"==typeof c&&!c.apply(b,[a])||"string"==typeof c&&!new RegExp(c).test(a)||c instanceof RegExp&&!c.test(a))}}),L.count=0,L.defaults={options:[],optgroups:[],plugins:[],delimiter:",",splitOn:null,persist:!0,diacritics:!0,create:!1,createOnBlur:!1,createFilter:null,highlight:!0,openOnFocus:!0,maxOptions:1e3,maxItems:null,hideSelected:null,addPrecedence:!1,selectOnTab:!1,preload:!1,allowEmptyOption:!1,closeAfterSelect:!1,scrollDuration:60,loadThrottle:300,loadingClass:"loading",dataAttr:"data-data",optgroupField:"optgroup",valueField:"value",labelField:"text",optgroupLabelField:"label",optgroupValueField:"value",lockOptgroupOrder:!1,sortField:"$order",searchField:["text"],searchConjunction:"and",mode:null,wrapperClass:"selectize-control",inputClass:"selectize-input",dropdownClass:"selectize-dropdown",dropdownContentClass:"selectize-dropdown-content",dropdownParent:null,copyClassesToDropdown:!0,render:{}},a.fn.selectize=function(b){var c=a.fn.selectize.defaults,d=a.extend({},c,b),e=d.dataAttr,f=d.labelField,g=d.valueField,h=d.optgroupField,i=d.optgroupLabelField,j=d.optgroupValueField,k=function(b,c){var h,i,j,k,l=b.attr(e);if(l)for(c.options=JSON.parse(l),h=0,i=c.options.length;i>h;h++)c.items.push(c.options[h][g]);else{var m=a.trim(b.val()||"");if(!d.allowEmptyOption&&!m.length)return;for(j=m.split(d.delimiter),h=0,i=j.length;i>h;h++)k={},k[f]=j[h],k[g]=j[h],c.options.push(k);c.items=j}},l=function(b,c){var k,l,m,n,o=c.options,p={},q=function(a){var b=e&&a.attr(e);return"string"==typeof b&&b.length?JSON.parse(b):null},r=function(b,e){b=a(b);var i=z(b.attr("value"));if(i||d.allowEmptyOption)if(p.hasOwnProperty(i)){if(e){var j=p[i][h];j?a.isArray(j)?j.push(e):p[i][h]=[j,e]:p[i][h]=e}}else{var k=q(b)||{};k[f]=k[f]||b.text(),k[g]=k[g]||i,k[h]=k[h]||e,p[i]=k,o.push(k),b.is(":selected")&&c.items.push(i)}},s=function(b){var d,e,f,g,h;for(b=a(b),f=b.attr("label"),f&&(g=q(b)||{},g[i]=f,g[j]=f,c.optgroups.push(g)),h=a("option",b),d=0,e=h.length;e>d;d++)r(h[d],f)};for(c.maxItems=b.attr("multiple")?null:1,n=b.children(),k=0,l=n.length;l>k;k++)m=n[k].tagName.toLowerCase(),"optgroup"===m?s(n[k]):"option"===m&&r(n[k])};return this.each(function(){if(!this.selectize){var e,f=a(this),g=this.tagName.toLowerCase(),h=f.attr("placeholder")||f.attr("data-placeholder");h||d.allowEmptyOption||(h=f.children('option[value=""]').text());var i={placeholder:h,options:[],optgroups:[],items:[]};"select"===g?l(f,i):k(f,i),e=new L(f,a.extend(!0,{},c,i,b))}})},a.fn.selectize.defaults=L.defaults,a.fn.selectize.support={validity:x},L.define("drag_drop",function(){if(!a.fn.sortable)throw new Error('The "drag_drop" plugin requires jQuery UI "sortable".');if("multi"===this.settings.mode){var b=this;b.lock=function(){var a=b.lock;return function(){var c=b.$control.data("sortable");return c&&c.disable(),a.apply(b,arguments)}}(),b.unlock=function(){var a=b.unlock;return function(){var c=b.$control.data("sortable");return c&&c.enable(),a.apply(b,arguments)}}(),b.setup=function(){var c=b.setup;return function(){c.apply(this,arguments);var d=b.$control.sortable({items:"[data-value]",forcePlaceholderSize:!0,disabled:b.isLocked,start:function(a,b){b.placeholder.css("width",b.helper.css("width")),d.css({overflow:"visible"})},stop:function(){d.css({overflow:"hidden"});var c=b.$activeItems?b.$activeItems.slice():null,e=[];d.children("[data-value]").each(function(){e.push(a(this).attr("data-value"))}),b.setValue(e),b.setActiveItem(c)}})}}()}}),L.define("dropdown_header",function(b){var c=this;b=a.extend({title:"Untitled",headerClass:"selectize-dropdown-header",titleRowClass:"selectize-dropdown-header-title",labelClass:"selectize-dropdown-header-label",closeClass:"selectize-dropdown-header-close",html:function(a){return'
'+a.title+'×
'}},b),c.setup=function(){var d=c.setup;return function(){d.apply(c,arguments),c.$dropdown_header=a(b.html(b)),c.$dropdown.prepend(c.$dropdown_header)}}()}),L.define("optgroup_columns",function(b){var c=this;b=a.extend({equalizeWidth:!0,equalizeHeight:!0},b),this.getAdjacentOption=function(b,c){var d=b.closest("[data-group]").find("[data-selectable]"),e=d.index(b)+c;return e>=0&&e
',a=a.firstChild,c.body.appendChild(a),b=d.width=a.offsetWidth-a.clientWidth,c.body.removeChild(a)),b},e=function(){var e,f,g,h,i,j,k;if(k=a("[data-group]",c.$dropdown_content),f=k.length,f&&c.$dropdown_content.width()){if(b.equalizeHeight){for(g=0,e=0;f>e;e++)g=Math.max(g,k.eq(e).height());k.css({height:g})}b.equalizeWidth&&(j=c.$dropdown_content.innerWidth()-d(),h=Math.round(j/f),k.css({width:h}),f>1&&(i=j-h*(f-1),k.eq(f-1).css({width:i})))}};(b.equalizeHeight||b.equalizeWidth)&&(C.after(this,"positionDropdown",e),C.after(this,"refreshOptions",e))}),L.define("remove_button",function(b){if("single"!==this.settings.mode){b=a.extend({label:"×",title:"Remove",className:"remove",append:!0},b);var c=this,d=''+b.label+"",e=function(a,b){var c=a.search(/(<\/[^>]+>\s*)$/);return a.substring(0,c)+b+a.substring(c)};this.setup=function(){var f=c.setup;return function(){if(b.append){var g=c.settings.render.item;c.settings.render.item=function(){return e(g.apply(this,arguments),d)}}f.apply(this,arguments),this.$control.on("click","."+b.className,function(b){if(b.preventDefault(),!c.isLocked){var d=a(b.currentTarget).parent();c.setActiveItem(d),c.deleteSelection()&&c.setCaret(c.items.length)}})}}()}}),L.define("restore_on_backspace",function(a){var b=this;a.text=a.text||function(a){return a[this.settings.labelField]},this.onKeyDown=function(){var c=b.onKeyDown;return function(b){var d,e;return b.keyCode===p&&""===this.$control_input.val()&&!this.$activeItems.length&&(d=this.caretPos-1,d>=0&&d + @@ -40,45 +41,6 @@ from plexpy import common
-

Plex Media Server

-
-

Enter your Plex Server details and then click the Verify button to make sure PlexPy can reach the server.

-
- -
-
- -
-
-
-
- -
-
- -
-
-
- -
-
-
-
- -
-
-
-
- - - Verify -
- -

Plex Authentication

Enter your Plex.tv username and password. PlexPy does not store your username or password.

@@ -100,6 +62,46 @@ from plexpy import common Authenticate
+
+

Plex Media Server

+ +

Enter your Plex Server details and then click the Verify button to make sure PlexPy can reach the server.

+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+
+
+ +
+
+
+
+ + + Verify +
+ +

Monitoring

@@ -184,6 +186,7 @@ from plexpy import common + - \ No newline at end of file + diff --git a/plexpy/plextv.py b/plexpy/plextv.py index cac4009a..a396e72d 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # This file is part of PlexPy. # # PlexPy is free software: you can redistribute it and/or modify @@ -14,7 +17,8 @@ # along with PlexPy. If not, see . from plexpy import logger, helpers, users, http_handler, database - +import xmltodict +import json from xml.dom import minidom import base64 @@ -401,4 +405,21 @@ class PlexTV(object): server_urls.append(server_details) - return server_urls \ No newline at end of file + return server_urls + + def discover(self): + """ Query plex for all servers online. Returns the ones you own in a selectize format """ + result = self.get_plextv_resources(include_https=True, output_format='raw') + clean_servers = [] + servers = xmltodict.parse(result, process_namespaces=True, attr_prefix='') + if servers: + for server in servers['MediaContainer']['Device']: + if server.get('presence', None) == '1' and server.get('owned', None) == '1': + for s in server['Connection']: + s.update(server) + s['value'] = s['address'] + s['label'] = server['name'] + del s['Connection'] + clean_servers.append(s) + + return json.dumps(clean_servers, indent=4) diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 46bb3dc2..480dd0f8 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -1,4 +1,7 @@ -# This file is part of PlexPy. +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This file is part of PlexPy. # # PlexPy is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -97,8 +100,7 @@ class WebInterface(object): } # The setup wizard just refreshes the page on submit so we must redirect to home if config set. - # Also redirecting to home if a PMS token already exists - will remove this in future. - if plexpy.CONFIG.FIRST_RUN_COMPLETE or plexpy.CONFIG.PMS_TOKEN: + if plexpy.CONFIG.FIRST_RUN_COMPLETE: plexpy.initialize_scheduler() raise cherrypy.HTTPRedirect("home") else: @@ -566,7 +568,7 @@ class WebInterface(object): watched_percent = plexpy.CONFIG.NOTIFY_WATCHED_PERCENT - custom_where=[] + custom_where = [] if user_id: custom_where = [['session_history.user_id', user_id]] elif user: @@ -887,7 +889,7 @@ class WebInterface(object): @cherrypy.expose def get_user_ips(self, user_id=None, user=None, **kwargs): - custom_where=[] + custom_where = [] if user_id: custom_where = [['user_id', user_id]] elif user: @@ -1121,7 +1123,7 @@ class WebInterface(object): pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_server_children() - + if result: cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps(result) @@ -1386,8 +1388,8 @@ class WebInterface(object): new_key_list = pms_connect.get_rating_keys_list(rating_key=new_rating_key, media_type=media_type) update_db = data_factory.update_rating_key(old_key_list=old_key_list, - new_key_list=new_key_list, - media_type=media_type) + new_key_list=new_key_list, + media_type=media_type) if update_db: cherrypy.response.headers['Content-type'] = 'application/json' @@ -1396,7 +1398,6 @@ class WebInterface(object): cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps({'message': 'no data received'}) - # test code @cherrypy.expose def get_new_rating_keys(self, rating_key='', media_type='', **kwargs): @@ -1433,11 +1434,27 @@ class WebInterface(object): new_key_list = pms_connect.get_rating_keys_list(rating_key=new_rating_key, media_type=media_type) result = data_factory.update_rating_key(old_key_list=old_key_list, - new_key_list=new_key_list, - media_type=media_type) + new_key_list=new_key_list, + media_type=media_type) if result: cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps(result) else: logger.warn('Unable to retrieve data.') + + @cherrypy.expose + def discover(self, token=''): + """ + Returns the servers that you own as a + list of dicts (formatted for selectize) + """ + # Need to set token so result dont return http 401 + plexpy.CONFIG.__setattr__('PMS_TOKEN', token) + plexpy.CONFIG.write() + + result = plextv.PlexTV() + servers = result.discover() + if servers: + cherrypy.response.headers['Content-type'] = 'application/json' + return servers From f7bffdc0503d52ca6c9c1302951e50280b0a3905 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 22 Sep 2015 02:04:11 +0200 Subject: [PATCH 02/24] Add stuff to api getLog finished --- API.md | 57 +++++- PlexPy.py | 2 + plexpy/api.py | 554 +++++++++++++++++++++++++++++++++++++------------- 3 files changed, 471 insertions(+), 142 deletions(-) diff --git a/API.md b/API.md index 808bb35a..ecdc127b 100644 --- a/API.md +++ b/API.md @@ -4,24 +4,77 @@ The API is still pretty new and needs some serious cleaning up on the backend bu ## General structure The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command` -Data response in JSON formatted. +Response example +``` +{ + "response": { + "data": [ + { + "loglevel": "INFO", + "msg": "Signal 2 caught, saving and exiting...", + "thread": "MainThread", + "time": "22-sep-2015 01:42:56 " + } + ], + "message": null, + "result": "success" + } +} +``` + +General parameters: + out_type: 'xml', + callback: 'pong', + 'debug': 1 + ## API methods ### getLogs -Not working yet +Possible params: sort='', search='', order='desc', regex='', start=0, end=0 +Returns the plexpy log + +### getApikey +Possible params: username='', password='' (required if auth is enabled) +Returns the apikey + +### getSettings +No params +Returns the config file ### getVersion +No params Returns some version information: git_path, install_type, current_version, installed_version, commits_behind +### getHistory +possible params: user=None, user_id=None, ,rating_key='', parent_rating_key='', grandparent_rating_key='', start_date='' +Returns + +### getMetadata +Required params: rating_key +Returns metadata about a file + +### getSync +Possible params: machine_id=None, user_id=None, +Returns + +### getUserips +Possible params: user_id=None, user=None + +### getPlayby +Possible params: time_range=30, y_axis='plays', playtype='total_plays_per_month' + ### checkGithub Updates the version information above and returns getVersion data ### shutdown +No params Shut down plexpy ### restart +No params Restart plexpy ### update +No params Update plexpy - you may want to check the install type in get version and not allow this if type==exe diff --git a/PlexPy.py b/PlexPy.py index 4d8d953f..cafb7804 100755 --- a/PlexPy.py +++ b/PlexPy.py @@ -1,4 +1,6 @@ #!/usr/bin/env python +# -*- coding: utf-8 -*- + # This file is part of PlexPy. # # PlexPy is free software: you can redistribute it and/or modify diff --git a/plexpy/api.py b/plexpy/api.py index 72ef82f8..cd0cdcd3 100644 --- a/plexpy/api.py +++ b/plexpy/api.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + # This file is part of PlexPy. # # PlexPy is free software: you can redistribute it and/or modify @@ -13,86 +16,143 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import db, cache, versioncheck, logger, helpers - +from plexpy import versioncheck, logger, plextv, pmsconnect, datafactory, graphs, users +import os import plexpy import json -from xml.dom import minidom +import traceback +import cherrypy +import re +import hashlib +import random +import xmltodict -cmd_list = ['getHistory', 'getLogs', 'getVersion', 'checkGithub', 'shutdown', 'restart', 'update'] +cmd_list = ['getLogs', 'getVersion', 'checkGithub', 'shutdown', + 'getSettings', 'restart', 'update', 'getApikey', 'getHistory', + 'getMetadata', 'getUserips', 'getPlayby'] class Api(object): - - def __init__(self): + def __init__(self, out='json'): self.apikey = None + self.authenticated = False self.cmd = None - self.id = None - self.kwargs = None - + # For the responses self.data = None - + self.msg = None + self.result_type = 'error' + # Possible general params self.callback = None + self.out_type = out + self.debug = None def checkParams(self, *args, **kwargs): if not plexpy.CONFIG.API_ENABLED: - self.data = 'API not enabled' - return - if not plexpy.CONFIG.API_KEY: - self.data = 'API key not generated' - return - if len(plexpy.CONFIG.API_KEY) != 32: - self.data = 'API key not generated correctly' - return + self.msg = 'API not enabled' + elif not plexpy.CONFIG.API_KEY: + self.msg = 'API key not generated' + elif len(plexpy.CONFIG.API_KEY) != 32: + self.msg = 'API key not generated correctly' + elif 'apikey' not in kwargs: + self.msg = 'Parameter apikey is required' + elif kwargs.get('apikey', '') != plexpy.CONFIG.API_KEY: + self.msg = 'Invalid apikey' + elif 'cmd' not in kwargs: + self.msg = 'Parameter %s required. possible commands are: %s' % ', '.join(cmd_list) + elif 'cmd' in kwargs and kwargs.get('cmd') not in cmd_list: + self.msg = 'Unknown command, %s possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(cmd_list)) - if 'apikey' not in kwargs: - self.data = 'Missing api key' - return + # Set default values or remove them from kwargs - if kwargs['apikey'] != plexpy.CONFIG.API_KEY: - self.data = 'Incorrect API key' - return - else: - self.apikey = kwargs.pop('apikey') + self.callback = kwargs.pop('callback', None) + self.apikey = kwargs.pop('apikey', None) + self.cmd = kwargs.pop('cmd', None) + self.debug = kwargs.pop('debug', False) + # Allow override for the api. + self.out_type = kwargs.pop('out_type', 'json') - if 'cmd' not in kwargs: - self.data = 'Missing parameter: cmd' - return - - if kwargs['cmd'] not in cmd_list: - self.data = 'Unknown command: %s' % kwargs['cmd'] - return - else: - self.cmd = kwargs.pop('cmd') + if self.apikey == plexpy.CONFIG.API_KEY and plexpy.CONFIG.API_ENABLED and self.cmd in cmd_list: + self.authenticated = True + self.msg = None + elif self.cmd == 'getApikey' and plexpy.CONFIG.API_ENABLED: + self.authenticated = True + # Remove the old error msg + self.msg = None self.kwargs = kwargs - self.data = 'OK' + + def _responds(self, result_type='success', data=None, msg=''): + + if data is None: + data = {} + return {"response": {"result": result_type, "message": msg, "data": data}} + + def _out_as(self, out): + + if self.out_type == 'json': + cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8' + try: + out = json.dumps(out, indent=4, sort_keys=True) + if self.callback is not None: + cherrypy.response.headers['Content-Type'] = 'application/javascript' + # wrap with JSONP call if requested + out = self.callback + '(' + out + ');' + # if we fail to generate the output fake an error + except Exception as e: + logger.info(u"API :: " + traceback.format_exc()) + out['message'] = traceback.format_exc() + out['result'] = 'error' + if self.out_type == 'xml': + cherrypy.response.headers['Content-Type'] = 'application/xml' + try: + out = xmltodict.unparse(out, pretty=True) + except ValueError as e: + logger.error('Failed to parse xml result') + try: + out['message'] = e + out['result'] = 'error' + out = xmltodict.unparse(out, pretty=True) + + except Exception as e: + logger.error('Failed to parse xml result error message') + out = ''' + + %s + + error + + ''' % e + + return out def fetchData(self): - if self.data == 'OK': - logger.info('Recieved API command: %s', self.cmd) - methodToCall = getattr(self, "_" + self.cmd) - methodToCall(**self.kwargs) - if 'callback' not in self.kwargs: - if isinstance(self.data, basestring): - return self.data - else: - return json.dumps(self.data) + logger.info('Recieved API command: %s' % self.cmd) + if self.cmd and self.authenticated: + methodtocall = getattr(self, "_" + self.cmd) + # Let the traceback hit cherrypy so we can + # see the traceback there + if self.debug: + methodtocall(**self.kwargs) else: - self.callback = self.kwargs['callback'] - self.data = json.dumps(self.data) - self.data = self.callback + '(' + self.data + ');' - return self.data - else: - return self.data + try: + methodtocall(**self.kwargs) + except Exception as e: + logger.error(traceback.format_exc()) + + # Im just lazy, fix me plx + if self.data or isinstance(self.data, (dict, list)): + if len(self.data): + self.result_type = 'success' + + return self._out_as(self._responds(result_type=self.result_type, msg=self.msg, data=self.data)) def _dic_from_query(self, query): - myDB = db.DBConnection() + myDB = database.DBConnection() rows = myDB.select(query) rows_as_dic = [] @@ -103,104 +163,115 @@ class Api(object): return rows_as_dic - def _getHistory(self, iDisplayStart=0, iDisplayLength=100, sSearch="", iSortCol_0='0', sSortDir_0='asc', **kwargs): - iDisplayStart = int(iDisplayStart) - iDisplayLength = int(iDisplayLength) - filtered = [] - totalcount = 0 - myDB = db.DBConnection() - db_table = db.DBConnection().get_history_table_name() + def _getApikey(self, username='', password=''): + """ Returns api key, requires username and password is active """ - sortcolumn = 'time' - sortbyhavepercent = False - if iSortCol_0 == '1': - sortcolumn = 'user' - if iSortCol_0 == '2': - sortcolumn = 'platform' - elif iSortCol_0 == '3': - sortcolumn = 'ip_address' - elif iSortCol_0 == '4': - sortcolumn = 'title' - elif iSortCol_0 == '5': - sortcolumn = 'time' - elif iSortCol_0 == '6': - sortcolumn = 'paused_counter' - elif iSortCol_0 == '7': - sortcolumn = 'stopped' - elif iSortCol_0 == '8': - sortbyhavepercent = True - - if sSearch == "": - query = 'SELECT * from %s order by %s COLLATE NOCASE %s' % (db_table, sortcolumn, sSortDir_0) - filtered = myDB.select(query) - totalcount = len(filtered) - else: - query = 'SELECT * from ' + db_table + ' WHERE user LIKE "%' + sSearch + \ - '%" OR title LIKE "%' + sSearch + '%"' + 'ORDER BY %s COLLATE NOCASE %s' % (sortcolumn, sSortDir_0) - filtered = myDB.select(query) - totalcount = myDB.select('SELECT COUNT(*) from processed')[0][0] - - history = filtered[iDisplayStart:(iDisplayStart + iDisplayLength)] - rows = [] - for item in history: - row = {"date": item['time'], - "user": item["user"], - "platform": item["platform"], - "ip_address": item["ip_address"], - "title": item["title"], - "started": item["time"], - "paused": item["paused_counter"], - "stopped": item["stopped"], - "duration": "", - "percent_complete": 0, - } - - if item['paused_counter'] > 0: - row['paused'] = item['paused_counter'] + apikey = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32] + if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD: + if username == plexpy.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD: + if plexpy.CONFIG.API_KEY: + self.data = plexpy.CONFIG.API_KEY + else: + self.data = apikey + plexpy.CONFIG.API_KEY = apikey + plexpy.CONFIG.write() else: - row['paused'] = 0 + self.msg = 'Authentication is enabled, please add the correct username and password to the parameters' + else: + if plexpy.CONFIG.API_KEY: + self.data = plexpy.CONFIG.API_KEY + else: + # Make a apikey if the doesn't exist + self.data = apikey + plexpy.CONFIG.API_KEY = apikey + plexpy.CONFIG.write() - if item['time']: - if item['stopped'] > 0: - stopped = item['stopped'] - else: - stopped = 0 - if item['paused_counter'] > 0: - paused_counter = item['paused_counter'] - else: - paused_counter = 0 + return self.data - row['duration'] = stopped - item['time'] + paused_counter + def _getLogs(self, sort='', search='', order='desc', regex='', **kwargs): + """ + Returns the log + + Returns [{"response": + {"msg": "Hey", + "result": "success"}, + "data": [{"time": "29-sept.2015", + "thread: "MainThread", + "msg: "Called x from y", + "loglevel": "DEBUG" + } + ] + + } + ] + """ + logfile = os.path.join(plexpy.CONFIG.LOG_DIR, 'plexpy.log') + templog = [] + start = int(kwargs.get('start', 0)) + end = int(kwargs.get('end', 0)) + + if regex: + logger.debug('Filtering log using regex %s' % regex) + reg = re.compile('u' + regex, flags=re.I) + + for line in open(logfile, 'r').readlines(): + temp_loglevel_and_time = None try: - xml_parse = minidom.parseString(helpers.latinToAscii(item['xml'])) - except IOError, e: - logger.warn("Error parsing XML in PlexWatch db: %s" % e) + temp_loglevel_and_time = line.split('- ') + loglvl = temp_loglevel_and_time[1].split(' :')[0].strip() + tl_tread = line.split(' :: ') + if loglvl is None: + msg = line.replace('\n', '') + else: + msg = line.split(' : ')[1].replace('\n', '') + thread = tl_tread[1].split(' : ')[0] + except IndexError: + # We assume this is a traceback + tl = (len(templog) - 1) + templog[tl]['msg'] += line.replace('\n', '') + continue - xml_head = xml_parse.getElementsByTagName('opt') - if not xml_head: - logger.warn("Error parsing XML in PlexWatch db: %s" % e) + if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line: - for s in xml_head: - if s.getAttribute('duration') and s.getAttribute('viewOffset'): - view_offset = helpers.cast_to_float(s.getAttribute('viewOffset')) - duration = helpers.cast_to_float(s.getAttribute('duration')) - if duration > 0: - row['percent_complete'] = (view_offset / duration)*100 - else: - row['percent_complete'] = 0 - - rows.append(row) - - dict = {'iTotalDisplayRecords': len(filtered), - 'iTotalRecords': totalcount, - 'aaData': rows, + d = { + 'time': temp_loglevel_and_time[0], + 'loglevel': loglvl, + 'msg': msg.replace('\n', ''), + 'thread': thread } - self.data = json.dumps(dict) - #cherrypy.response.headers['Content-type'] = 'application/json' + templog.append(d) - def _getLogs(self, **kwargs): - pass + if end > 0: + logger.debug('Slicing the log from %s to %s' % (start, end)) + templog = templog[start:end] + + if sort: + logger.debug('Sorting log based on %s' % sort) + templog = sorted(templog, key=lambda k: k[sort]) + + if search: + logger.debug('Searching log values for %s' % search) + tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()] + + if len(tt): + templog = tt + + if regex: + tt = [] + for l in templog: + stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items()) + if reg.search(stringdict): + tt.append(l) + + if len(tt): + templog = tt + + if order == 'desc': + templog = templog[::-1] + + self.data = templog + return templog def _getVersion(self, **kwargs): self.data = { @@ -210,6 +281,7 @@ class Api(object): 'latest_version': plexpy.LATEST_VERSION, 'commits_behind': plexpy.COMMITS_BEHIND, } + self.result_type = 'success' def _checkGithub(self, **kwargs): versioncheck.checkGithub() @@ -217,9 +289,211 @@ class Api(object): def _shutdown(self, **kwargs): plexpy.SIGNAL = 'shutdown' + self.msg = 'Shutting down plexpy' + self.result_type = 'success' def _restart(self, **kwargs): plexpy.SIGNAL = 'restart' + self.msg = 'Restarting plexpy' + self.result_type = 'success' def _update(self, **kwargs): plexpy.SIGNAL = 'update' + self.msg = 'Updating plexpy' + self.result_type = 'success' + + def _getHistory(self, user=None, user_id=None, rating_key='', parent_rating_key='', grandparent_rating_key='', start_date='', **kwargs): + + custom_where = [] + if user_id: + custom_where = [['user_id', user_id]] + elif user: + custom_where = [['user', user]] + if 'rating_key' in kwargs: + rating_key = kwargs.get('rating_key', "") + custom_where = [['rating_key', rating_key]] + if 'parent_rating_key' in kwargs: + rating_key = kwargs.get('parent_rating_key', "") + custom_where = [['parent_rating_key', rating_key]] + if 'grandparent_rating_key' in kwargs: + rating_key = kwargs.get('grandparent_rating_key', "") + custom_where = [['grandparent_rating_key', rating_key]] + if 'start_date' in kwargs: + start_date = kwargs.get('start_date', "") + custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]] + + data_factory = datafactory.DataFactory() + history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where) + + self.data = history + return self.data + + def _getSync(self, machine_id=None, user_id=None, **kwargs): + + pms_connect = pmsconnect.PmsConnect() + server_id = pms_connect.get_server_identity() + + plex_tv = plextv.PlexTV() + if not machine_id: + result = plex_tv.get_synced_items(machine_id=server_id['machine_identifier'], user_id=user_id) + else: + result = plex_tv.get_synced_items(machine_id=machine_id, user_id=user_id) + + if result: + self.data = result + return result + else: + self.msg = 'Unable to retrieve sync data for user' + logger.warn('Unable to retrieve sync data for user.') + + def _getMetadata(self, rating_key='', **kwargs): + + pms_connect = pmsconnect.PmsConnect() + result = pms_connect.get_metadata(rating_key, 'dict') + + if result: + self.data = result + return result + else: + self.msg = 'Unable to retrive metadata %s' % rating_key + logger.warn('Unable to retrieve data.') + + def _getSettings(self): + interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/') + interface_list = [name for name in os.listdir(interface_dir) if + os.path.isdir(os.path.join(interface_dir, name))] + + config = { + "http_host": plexpy.CONFIG.HTTP_HOST, + "http_username": plexpy.CONFIG.HTTP_USERNAME, + "http_port": plexpy.CONFIG.HTTP_PORT, + "http_password": plexpy.CONFIG.HTTP_PASSWORD, + "launch_browser": bool(plexpy.CONFIG.LAUNCH_BROWSER), + "enable_https": bool(plexpy.CONFIG.ENABLE_HTTPS), + "https_cert": plexpy.CONFIG.HTTPS_CERT, + "https_key": plexpy.CONFIG.HTTPS_KEY, + "api_enabled": plexpy.CONFIG.API_ENABLED, + "api_key": plexpy.CONFIG.API_KEY, + "update_db_interval": plexpy.CONFIG.UPDATE_DB_INTERVAL, + "freeze_db": bool(plexpy.CONFIG.FREEZE_DB), + "log_dir": plexpy.CONFIG.LOG_DIR, + "cache_dir": plexpy.CONFIG.CACHE_DIR, + "check_github": bool(plexpy.CONFIG.CHECK_GITHUB), + "interface_list": interface_list, + "cache_sizemb": plexpy.CONFIG.CACHE_SIZEMB, + "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER, + "pms_ip": plexpy.CONFIG.PMS_IP, + "pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER, + "pms_port": plexpy.CONFIG.PMS_PORT, + "pms_token": plexpy.CONFIG.PMS_TOKEN, + "pms_ssl": bool(plexpy.CONFIG.PMS_SSL), + "pms_use_bif": bool(plexpy.CONFIG.PMS_USE_BIF), + "pms_uuid": plexpy.CONFIG.PMS_UUID, + "date_format": plexpy.CONFIG.DATE_FORMAT, + "time_format": plexpy.CONFIG.TIME_FORMAT, + "grouping_global_history": bool(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY), + "grouping_user_history": bool(plexpy.CONFIG.GROUPING_USER_HISTORY), + "grouping_charts": bool(plexpy.CONFIG.GROUPING_CHARTS), + "tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE), + "movie_notify_enable": bool(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE), + "music_notify_enable": bool(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE), + "tv_notify_on_start": bool(plexpy.CONFIG.TV_NOTIFY_ON_START), + "movie_notify_on_start": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_START), + "music_notify_on_start": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_START), + "tv_notify_on_stop": bool(plexpy.CONFIG.TV_NOTIFY_ON_STOP), + "movie_notify_on_stop": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_STOP), + "music_notify_on_stop": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_STOP), + "tv_notify_on_pause": bool(plexpy.CONFIG.TV_NOTIFY_ON_PAUSE), + "movie_notify_on_pause": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_PAUSE), + "music_notify_on_pause": bool(plexpy.CONFIG.MUSIC_NOTIFY_ON_PAUSE), + "monitoring_interval": plexpy.CONFIG.MONITORING_INTERVAL, + "refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL, + "refresh_users_on_startup": bool(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP), + "ip_logging_enable": bool(plexpy.CONFIG.IP_LOGGING_ENABLE), + "video_logging_enable": bool(plexpy.CONFIG.VIDEO_LOGGING_ENABLE), + "music_logging_enable": bool(plexpy.CONFIG.MUSIC_LOGGING_ENABLE), + "logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL, + "pms_is_remote": bool(plexpy.CONFIG.PMS_IS_REMOTE), + "notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT, + "notify_on_start_subject_text": plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT, + "notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT, + "notify_on_stop_subject_text": plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT, + "notify_on_stop_body_text": plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT, + "notify_on_pause_subject_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT, + "notify_on_pause_body_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT, + "notify_on_resume_subject_text": plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT, + "notify_on_resume_body_text": plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT, + "notify_on_buffer_subject_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT, + "notify_on_buffer_body_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT, + "notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT, + "notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT, + "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, + "home_stats_type": bool(plexpy.CONFIG.HOME_STATS_TYPE), + "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, + "home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS, + "home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS, + "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, + "buffer_wait": plexpy.CONFIG.BUFFER_WAIT + } + + self.data = config + return config + + def _getUserips(self, user_id=None, user=None, **kwargs): + custom_where = [] + if user_id: + custom_where = [['user_id', user_id]] + elif user: + custom_where = [['user', user]] + + user_data = users.Users() + history = user_data.get_user_unique_ips(kwargs=kwargs, + custom_where=custom_where) + + if history: + self.data = history + return history + else: + self.msg = 'Failed to find users ips' + + def _getPlayby(self, time_range=30, y_axis='plays', playtype='total_plays_per_month', **kwargs): + + graph = graphs.Graphs() + if playtype == 'total_plays_per_month': + result = graph.get_total_plays_per_month(y_axis=y_axis) + + elif playtype == 'total_plays_per_day': + result = graph.get_total_plays_per_day(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_per_hourofday': + result = graph.get_total_plays_per_hourofday(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_per_dayofweek': + result = graph.get_total_plays_per_dayofweek(time_range=time_range, y_axis=y_axis) + + elif playtype == 'stream_type_by_top_10_users': + result = graph.get_stream_type_by_top_10_users(time_range=time_range, y_axis=y_axis) + + elif playtype == 'stream_type_by_top_10_platforms': + result = graph.get_stream_type_by_top_10_platforms(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_by_stream_resolution': + result = graph.get_total_plays_by_stream_resolution(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_by_source_resolution': + result = graph.get_total_plays_by_source_resolution(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_per_stream_type': + result = graph.get_total_plays_per_stream_type(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_by_top_10_users': + result = graph.get_total_plays_by_top_10_users(time_range=time_range, y_axis=y_axis) + + elif playtype == 'total_plays_by_top_10_platforms': + result = graph.get_total_plays_by_top_10_platforms(time_range=time_range, y_axis=y_axis) + + if result: + self.data = result + return result + else: + logger.warn('Unable to retrieve %s from db' % playtype) From ff8d9f9f4c589a56d05b9065f32c86b135319a68 Mon Sep 17 00:00:00 2001 From: John Date: Tue, 29 Sep 2015 23:16:34 +0200 Subject: [PATCH 03/24] fix parsing of servers, remove unbound var --- plexpy/plextv.py | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/plexpy/plextv.py b/plexpy/plextv.py index a396e72d..03372fdc 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.py @@ -24,6 +24,7 @@ from xml.dom import minidom import base64 import plexpy + def refresh_users(): logger.info("Requesting users list refresh...") result = PlexTV().get_full_users_list() @@ -58,6 +59,7 @@ def refresh_users(): else: logger.warn("Unable to refresh users list.") + def get_real_pms_url(): logger.info("Requesting URLs for server...") @@ -95,6 +97,7 @@ def get_real_pms_url(): plexpy.CONFIG.__setattr__('PMS_URL', fallback_url) plexpy.CONFIG.write() + class PlexTV(object): """ Plex.tv authentication @@ -137,7 +140,7 @@ class PlexTV(object): if plextv_response: xml_head = plextv_response.getElementsByTagName('user') if not xml_head: - logger.warn("Error parsing XML for Plex.tv token: %s" % e) + logger.warn("Error parsing XML for Plex.tv token") return [] auth_token = xml_head[0].getAttribute('authenticationToken') @@ -410,16 +413,34 @@ class PlexTV(object): def discover(self): """ Query plex for all servers online. Returns the ones you own in a selectize format """ result = self.get_plextv_resources(include_https=True, output_format='raw') - clean_servers = [] servers = xmltodict.parse(result, process_namespaces=True, attr_prefix='') - if servers: - for server in servers['MediaContainer']['Device']: - if server.get('presence', None) == '1' and server.get('owned', None) == '1': - for s in server['Connection']: - s.update(server) - s['value'] = s['address'] - s['label'] = server['name'] - del s['Connection'] - clean_servers.append(s) + clean_servers = [] + + try: + if servers: + # Fix if its only one "device" + if int(servers['MediaContainer']['size']) == 1: + servers['MediaContainer']['Device'] = [servers['MediaContainer']['Device']] + + for server in servers['MediaContainer']['Device']: + # Only grab servers online and own + if server.get('presence', None) == '1' and server.get('owned', None) == '1' and server.get('provides', None) == 'server': + # If someone only has one connection.. + if isinstance(server['Connection'], dict): + server['Connection'] = [server['Connection']] + + for s in server['Connection']: + # to avoid circular ref + d = {} + d.update(s) + d.update(server) + d['label'] = d['name'] + d['value'] = d['address'] + del d['Connection'] + clean_servers.append(d) + + except Exception as e: + logger.warn('Failed to get servers from plex %s' % e) + return clean_servers return json.dumps(clean_servers, indent=4) From 9d925cce03c559dbc2d9fb5b1077916e62316f3c Mon Sep 17 00:00:00 2001 From: John Date: Tue, 29 Sep 2015 23:27:19 +0200 Subject: [PATCH 04/24] add getSync, fix named argument --- plexpy/api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plexpy/api.py b/plexpy/api.py index cd0cdcd3..bb420caa 100644 --- a/plexpy/api.py +++ b/plexpy/api.py @@ -29,7 +29,7 @@ import xmltodict cmd_list = ['getLogs', 'getVersion', 'checkGithub', 'shutdown', 'getSettings', 'restart', 'update', 'getApikey', 'getHistory', - 'getMetadata', 'getUserips', 'getPlayby'] + 'getMetadata', 'getUserips', 'getPlayby, getSync'] class Api(object): @@ -456,7 +456,7 @@ class Api(object): else: self.msg = 'Failed to find users ips' - def _getPlayby(self, time_range=30, y_axis='plays', playtype='total_plays_per_month', **kwargs): + def _getPlayby(self, time_range='30', y_axis='plays', playtype='total_plays_per_month', **kwargs): graph = graphs.Graphs() if playtype == 'total_plays_per_month': From e884d018eddb794de5d0fcf1c6cfa15fc0c20e33 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 29 Sep 2015 12:35:52 -0700 Subject: [PATCH 05/24] Fix indents --- data/interfaces/default/js/script.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/data/interfaces/default/js/script.js b/data/interfaces/default/js/script.js index 142121b9..34b7be20 100644 --- a/data/interfaces/default/js/script.js +++ b/data/interfaces/default/js/script.js @@ -217,13 +217,13 @@ function getPlatformImagePath(platformName) { return 'interfaces/default/images/platforms/playstation.png'; } else if (platformName.indexOf("Playstation 4") > -1) { return 'interfaces/default/images/platforms/playstation.png'; - } else if (platformName.indexOf("Xbox 360") > -1) { + } else if (platformName.indexOf("Xbox 360") > -1) { return 'interfaces/default/images/platforms/xbox.png'; - } else if (platformName.indexOf("Windows") > -1) { - return 'interfaces/default/images/platforms/win8.png'; - } else if (platformName.indexOf("Windows phone") > -1) { - return 'interfaces/default/images/platforms/wp.png'; - } else { + } else if (platformName.indexOf("Windows") > -1) { + return 'interfaces/default/images/platforms/win8.png'; + } else if (platformName.indexOf("Windows phone") > -1) { + return 'interfaces/default/images/platforms/wp.png'; + } else { return 'interfaces/default/images/platforms/default.png'; } } From f1c4bf62492d587d5a5d0bdf925b88a6cd7e1e17 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 29 Sep 2015 20:43:08 -0700 Subject: [PATCH 06/24] Fix current activity details for tracks --- data/interfaces/default/current_activity.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index 12bb2f84..b118d47f 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -137,9 +137,9 @@ DOCUMENTATION :: END
% if a['audio_decision'] == 'direct play': Audio  Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch) - % elif a['audio_decision'] == 'Copy': + % elif a['audio_decision'] == 'copy': Audio  Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) - % elif a['audio_decision'] != 'transcode': + % elif a['audio_decision'] == 'transcode': Audio  Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) % endif % elif a['media_type'] == 'episode' or a['media_type'] == 'movie' or a['media_type'] == 'clip': From 2616e14c834b94b4d88ffe19458e74777a3726c1 Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 29 Sep 2015 22:43:23 -0700 Subject: [PATCH 07/24] Add "player" to tables * Also rename user platforms to user players * Fix other platform vs. player ambiguities --- data/interfaces/default/css/plexpy.css | 14 ++++---- data/interfaces/default/history.html | 3 +- .../default/history_table_modal.html | 2 +- data/interfaces/default/info.html | 3 +- .../default/js/tables/history_table.js | 28 ++++++++++----- .../default/js/tables/sync_table.js | 8 ++--- data/interfaces/default/js/tables/user_ips.js | 18 ++++++++-- data/interfaces/default/js/tables/users.js | 27 +++++++++++---- data/interfaces/default/sync.html | 2 +- data/interfaces/default/user.html | 14 ++++---- ...form_stats.html => user_player_stats.html} | 17 +++++----- data/interfaces/default/users.html | 1 + plexpy/datafactory.py | 8 +++++ plexpy/users.py | 34 ++++++++++++++----- plexpy/webserve.py | 10 +++--- 15 files changed, 128 insertions(+), 61 deletions(-) rename data/interfaces/default/{user_platform_stats.html => user_player_stats.html} (57%) diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index fedbf8d4..b736ffe1 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -1565,19 +1565,19 @@ a:hover .item-children-poster { top: 3px; left: 3px; } -.user-platforms ul { +.user-player ul { list-style: none; margin: 0; } -.user-platforms-instance { +.user-player-instance { float: left; width: 240px; height: 80px; margin-bottom: 25px; } -.user-platforms-instance li { +.user-player-instance li { } -.user-platforms-instance-box { +.user-player-instance-box { float: left; width: 75px; border-radius: 3px; @@ -1589,7 +1589,7 @@ a:hover .item-children-poster { height: 80px; width: 80px; } -.user-platforms-instance-name { +.user-player-instance-name { float: left; padding-top: 14px; color: #fff; @@ -1602,7 +1602,7 @@ a:hover .item-children-poster { width: 140px; margin-left: 10px; } -.user-platforms-instance-playcount h3 { +.user-player-instance-playcount h3 { font-size: 30px; font-weight: bold; color: #F9AA03; @@ -1612,7 +1612,7 @@ a:hover .item-children-poster { margin: 0 5px 0 10px; float: left; } -.user-platforms-instance-playcount p { +.user-player-instance-playcount p { color: #aaa; font-size: 12px; float: left; diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index 0615ea28..3f589390 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -30,6 +30,7 @@ User IP Address Platform + Player Title Started Paused @@ -84,7 +85,7 @@ } } history_table = $('#history_table').DataTable(history_table_options); - var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] }); + var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); $(colvis.button()).appendTo('div.colvis-button-bar'); clearSearchButton('history_table', history_table); diff --git a/data/interfaces/default/history_table_modal.html b/data/interfaces/default/history_table_modal.html index 58345fcf..19de1faf 100644 --- a/data/interfaces/default/history_table_modal.html +++ b/data/interfaces/default/history_table_modal.html @@ -16,7 +16,7 @@ Started Stopped User - Platform + Player Title diff --git a/data/interfaces/default/info.html b/data/interfaces/default/info.html index 0dd8012f..0c9ad3d8 100644 --- a/data/interfaces/default/info.html +++ b/data/interfaces/default/info.html @@ -333,6 +333,7 @@ DOCUMENTATION :: END User IP Address Platform + Player Title Started Paused @@ -550,7 +551,7 @@ DOCUMENTATION :: END $(document).ready(function () { get_history(); history_table = $('#history_table').DataTable(history_table_options); - var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] }); + var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); $(colvis.button()).appendTo('div.colvis-button-bar'); clearSearchButton('history_table', history_table); diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js index ec086ba2..6ff1d674 100644 --- a/data/interfaces/default/js/tables/history_table.js +++ b/data/interfaces/default/js/tables/history_table.js @@ -100,7 +100,18 @@ history_table_options = { }, { "targets": [4], - "data":"player", + "data":"platform", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== '') { + $(td).html(cellData); + } + }, + "width": "8%", + "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" + }, + { + "targets": [5], + "data": "player", "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== '') { var transcode_dec = ''; @@ -114,11 +125,11 @@ history_table_options = { $(td).html(''); } }, - "width": "15%", + "width": "12%", "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" }, { - "targets": [5], + "targets": [6], "data":"full_title", "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== '') { @@ -145,7 +156,7 @@ history_table_options = { "width": "35%" }, { - "targets": [6], + "targets": [7], "data":"started", "createdCell": function (td, cellData, rowData, row, col) { if (cellData === null) { @@ -159,7 +170,7 @@ history_table_options = { "className": "no-wrap hidden-sm hidden-xs" }, { - "targets": [7], + "targets": [8], "data":"paused_counter", "render": function (data, type, full) { if (data !== null) { @@ -173,7 +184,7 @@ history_table_options = { "className": "no-wrap hidden-md hidden-sm hidden-xs" }, { - "targets": [8], + "targets": [9], "data":"stopped", "createdCell": function (td, cellData, rowData, row, col) { if (cellData === null) { @@ -187,7 +198,7 @@ history_table_options = { "className": "no-wrap hidden-sm hidden-xs" }, { - "targets": [9], + "targets": [10], "data":"duration", "render": function (data, type, full) { if (data !== null) { @@ -201,7 +212,7 @@ history_table_options = { "className": "no-wrap hidden-xs" }, { - "targets": [10], + "targets": [11], "data": "watched_status", "render": function (data, type, full) { if (data == 1) { @@ -462,6 +473,7 @@ function childTableFormat(rowData) { 'User' + 'IP Address' + 'Platform' + + 'Player' + 'Title' + 'Started' + 'Paused' + diff --git a/data/interfaces/default/js/tables/sync_table.js b/data/interfaces/default/js/tables/sync_table.js index 3aa118c6..f560eac7 100644 --- a/data/interfaces/default/js/tables/sync_table.js +++ b/data/interfaces/default/js/tables/sync_table.js @@ -67,13 +67,13 @@ sync_table_options = { }, { "targets": [4], - "data": "device_name", - "className": "no-wrap hidden-xs" + "data": "platform", + "className": "no-wrap hidden-sm hidden-xs" }, { "targets": [5], - "data": "platform", - "className": "no-wrap hidden-sm hidden-xs" + "data": "device_name", + "className": "no-wrap hidden-xs" }, { "targets": [6], diff --git a/data/interfaces/default/js/tables/user_ips.js b/data/interfaces/default/js/tables/user_ips.js index de008320..506fe43a 100644 --- a/data/interfaces/default/js/tables/user_ips.js +++ b/data/interfaces/default/js/tables/user_ips.js @@ -49,7 +49,18 @@ user_ip_table_options = { }, { "targets": [2], - "data":"platform", + "data": "platform", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== '') { + $(td).html(cellData); + } + }, + "width": "15%", + "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" + }, + { + "targets": [3], + "data":"player", "createdCell": function (td, cellData, rowData, row, col) { if (cellData) { var transcode_dec = ''; @@ -69,7 +80,7 @@ user_ip_table_options = { "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" }, { - "targets": [3], + "targets": [4], "data":"last_watched", "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== '') { @@ -94,10 +105,11 @@ user_ip_table_options = { } } }, + "width": "30%", "className": "hidden-sm hidden-xs" }, { - "targets": [4], + "targets": [5], "data":"play_count", "searchable": false, "width": "10%" diff --git a/data/interfaces/default/js/tables/users.js b/data/interfaces/default/js/tables/users.js index 04bf631b..78240a3e 100644 --- a/data/interfaces/default/js/tables/users.js +++ b/data/interfaces/default/js/tables/users.js @@ -64,7 +64,7 @@ users_list_table_options = { $(td).html(cellData); } }, - "width": "12%", + "width": "10%", "className": "edit-user-control no-wrap" }, { @@ -78,7 +78,7 @@ users_list_table_options = { } }, "searchable": false, - "width": "12%", + "width": "10%", "className": "no-wrap hidden-xs" }, { @@ -99,12 +99,25 @@ users_list_table_options = { $(td).html('n/a'); } }, - "width": "12%", + "width": "10%", "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip" }, { "targets": [5], - "data":"platform", + "data": "platform", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== '') { + $(td).html(cellData); + } else { + $(td).html('n/a'); + } + }, + "width": "10%", + "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" + }, + { + "targets": [6], + "data":"player", "createdCell": function (td, cellData, rowData, row, col) { if (cellData) { var transcode_dec = ''; @@ -120,11 +133,11 @@ users_list_table_options = { $(td).html('n/a'); } }, - "width": "12%", + "width": "15%", "className": "no-wrap hidden-md hidden-sm hidden-xs modal-control" }, { - "targets": [6], + "targets": [7], "data":"last_watched", "createdCell": function (td, cellData, rowData, row, col) { if (cellData !== '') { @@ -153,7 +166,7 @@ users_list_table_options = { "className": "hidden-sm hidden-xs" }, { - "targets": [7], + "targets": [8], "data": "plays", "searchable": false, "width": "10%" diff --git a/data/interfaces/default/sync.html b/data/interfaces/default/sync.html index 7378ad77..bad24c0c 100644 --- a/data/interfaces/default/sync.html +++ b/data/interfaces/default/sync.html @@ -29,8 +29,8 @@ Username Title Type - Device Platform + Device Total Size Total Items Converted diff --git a/data/interfaces/default/user.html b/data/interfaces/default/user.html index fa90d112..46babfaf 100644 --- a/data/interfaces/default/user.html +++ b/data/interfaces/default/user.html @@ -85,11 +85,11 @@ from plexpy import helpers
- Platform Stats + Player Stats
-
+
Loading data...

@@ -133,6 +133,7 @@ from plexpy import helpers Last Seen IP Address Last Platform + Last Player Last Watched Play Count @@ -170,6 +171,7 @@ from plexpy import helpers User IP Address Platform + Player Title Started Paused @@ -207,8 +209,8 @@ from plexpy import helpers Username Title Type - Device Platform + Device Total Size Total Items Converted @@ -309,11 +311,11 @@ from plexpy import helpers // Populate platform stats $.ajax({ - url: 'get_user_platform_stats', + url: 'get_user_player_stats', async: true, data: { user_id: user_id, user: '${data['username']}' }, complete: function(xhr, status) { - $("#user-platform-stats").html(xhr.responseText); + $("#user-player-stats").html(xhr.responseText); } }); @@ -332,7 +334,7 @@ from plexpy import helpers history_table = $('#history_table').DataTable(history_table_options); history_table.column(2).visible(false); - var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] }); + var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); $(colvis.button()).appendTo('#button-bar-history'); clearSearchButton('history_table', history_table); diff --git a/data/interfaces/default/user_platform_stats.html b/data/interfaces/default/user_player_stats.html similarity index 57% rename from data/interfaces/default/user_platform_stats.html rename to data/interfaces/default/user_player_stats.html index afd1d6b3..7aab53b6 100644 --- a/data/interfaces/default/user_platform_stats.html +++ b/data/interfaces/default/user_player_stats.html @@ -11,8 +11,9 @@ data[array_index] :: Usable parameters == Global keys == result_id Returns a unique identifier for the result. -platform_name Returns the name of the platform. -total_plays Returns the play count for the platform. +player_name Returns the name of the player. +platform_type Returns the name of the platform +total_plays Returns the play count for the player. DOCUMENTATION :: END @@ -20,13 +21,13 @@ DOCUMENTATION :: END % if data != None: % for a in data:
    -
    +
  • - -
    - ${a['platform_name']} + +
    + ${a['player_name']}
    -
    +

    ${a['total_plays']}

    plays

    @@ -34,7 +35,7 @@ DOCUMENTATION :: END
% endfor % else: diff --git a/data/interfaces/default/users.html b/data/interfaces/default/users.html index b493f997..f4bc65c2 100644 --- a/data/interfaces/default/users.html +++ b/data/interfaces/default/users.html @@ -29,6 +29,7 @@ Last Seen Last Known IP Last Platform + Last Player Last Watched Total Plays diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 7718de11..d10dc7e0 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -42,6 +42,7 @@ class DataFactory(object): 'session_history.user_id', 'session_history.user', '(CASE WHEN users.friendly_name IS NULL THEN user ELSE users.friendly_name END) as friendly_name', + 'platform', 'player', 'ip_address', 'session_history_metadata.media_type', @@ -104,6 +105,12 @@ class DataFactory(object): else: watched_status = 0 + # Rename Mystery platform names + platform_names = {'Mystery 3': 'Playstation 3', + 'Mystery 4': 'Playstation 4', + 'Mystery 5': 'Xbox 360'} + platform = platform_names.get(item["platform"], item["platform"]) + row = {"reference_id": item["reference_id"], "id": item["id"], "date": item["date"], @@ -114,6 +121,7 @@ class DataFactory(object): "user_id": item["user_id"], "user": item["user"], "friendly_name": item["friendly_name"], + "platform": platform, "player": item["player"], "ip_address": item["ip_address"], "media_type": item["media_type"], diff --git a/plexpy/users.py b/plexpy/users.py index d6b32c5b..1983a290 100644 --- a/plexpy/users.py +++ b/plexpy/users.py @@ -32,7 +32,8 @@ class Users(object): 'MAX(session_history.started) as last_seen', 'session_history.ip_address as ip_address', 'COUNT(session_history.id) as plays', - 'session_history.player as platform', + 'session_history.platform as platform', + 'session_history.player as player', 'session_history_metadata.full_title as last_watched', 'session_history_metadata.thumb', 'session_history_metadata.parent_thumb', @@ -83,12 +84,19 @@ class Users(object): else: user_thumb = item['user_thumb'] + # Rename Mystery platform names + platform_names = {'Mystery 3': 'Playstation 3', + 'Mystery 4': 'Playstation 4', + 'Mystery 5': 'Xbox 360'} + platform = platform_names.get(item["platform"], item["platform"]) + row = {"id": item['id'], "plays": item['plays'], "last_seen": item['last_seen'], "friendly_name": item['friendly_name'], "ip_address": item['ip_address'], - "platform": item['platform'], + "platform": platform, + "player": item['player'], "last_watched": item['last_watched'], "thumb": thumb, "media_type": item['media_type'], @@ -121,7 +129,8 @@ class Users(object): 'session_history.started as last_seen', 'session_history.ip_address as ip_address', 'COUNT(session_history.id) as play_count', - 'session_history.player as platform', + 'session_history.platform as platform', + 'session_history.player as player', 'session_history_metadata.full_title as last_watched', 'session_history_metadata.thumb', 'session_history_metadata.parent_thumb', @@ -169,11 +178,18 @@ class Users(object): else: thumb = item["thumb"] + # Rename Mystery platform names + platform_names = {'Mystery 3': 'Playstation 3', + 'Mystery 4': 'Playstation 4', + 'Mystery 5': 'Xbox 360'} + platform = platform_names.get(item["platform"], item["platform"]) + row = {"id": item['id'], "last_seen": item['last_seen'], "ip_address": item['ip_address'], "play_count": item['play_count'], - "platform": item['platform'], + "platform": platform, + "player": item['player'], "last_watched": item['last_watched'], "thumb": thumb, "media_type": item['media_type'], @@ -490,10 +506,10 @@ class Users(object): return user_watch_time_stats - def get_user_platform_stats(self, user=None, user_id=None): + def get_user_player_stats(self, user=None, user_id=None): monitor_db = database.MonitorDatabase() - platform_stats = [] + player_stats = [] result_id = 0 try: @@ -522,12 +538,12 @@ class Users(object): 'Mystery 5': 'Xbox 360'} platform_type = platform_names.get(item[2], item[2]) - row = {'platform_name': item[0], + row = {'player_name': item[0], 'platform_type': platform_type, 'total_plays': item[1], 'result_id': result_id } - platform_stats.append(row) + player_stats.append(row) result_id += 1 - return platform_stats \ No newline at end of file + return player_stats \ No newline at end of file diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 3129e493..7cdee875 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -813,17 +813,17 @@ class WebInterface(object): return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") @cherrypy.expose - def get_user_platform_stats(self, user=None, user_id=None, **kwargs): + def get_user_player_stats(self, user=None, user_id=None, **kwargs): user_data = users.Users() - result = user_data.get_user_platform_stats(user_id=user_id, user=user) + result = user_data.get_user_player_stats(user_id=user_id, user=user) if result: - return serve_template(templatename="user_platform_stats.html", data=result, - title="Platform Stats") + return serve_template(templatename="user_player_stats.html", data=result, + title="Player Stats") else: logger.warn('Unable to retrieve data.') - return serve_template(templatename="user_platform_stats.html", data=None, title="Platform Stats") + return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats") @cherrypy.expose def get_item_children(self, rating_key='', **kwargs): From 5b88058133c58decc81e545d27b4235bf211e70a Mon Sep 17 00:00:00 2001 From: Jonathan Wong Date: Tue, 29 Sep 2015 23:23:03 -0700 Subject: [PATCH 08/24] Add top level info pages with history table * Movies / TV Shows / Music --- data/interfaces/default/info.html | 51 +++++++++++++++++++++++-------- plexpy/webserve.py | 9 ++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/data/interfaces/default/info.html b/data/interfaces/default/info.html index 0c9ad3d8..0c681415 100644 --- a/data/interfaces/default/info.html +++ b/data/interfaces/default/info.html @@ -60,27 +60,37 @@ DOCUMENTATION :: END % if data:
+ % if data['type'] != 'library':
+ % endif
- % if data['type'] == 'movie': - Movies + % if data['type'] == 'library': + % if data['library'] == 'movie': + Movies + % elif data['library'] == 'show': + TV Shows + % elif data['library'] == 'music': + Music + % endif + % elif data['type'] == 'movie': + Movies ${data['title']} % elif data['type'] == 'show': - TV Shows + TV Shows ${data['title']} % elif data['type'] == 'season': - + ${data['parent_title']} Season ${data['index']} % elif data['type'] == 'episode': - + @@ -88,17 +98,17 @@ DOCUMENTATION :: END Episode ${data['index']} - ${data['title']} % elif data['type'] == 'artist': - Music + Music ${data['title']} % elif data['type'] == 'album': - + ${data['parent_title']} ${data['title']} % elif data['type'] == 'track': - + @@ -109,12 +119,13 @@ DOCUMENTATION :: END
+ % if data['type'] != 'library': + % endif
+ % if data['type'] != 'library':
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season': @@ -311,6 +324,7 @@ DOCUMENTATION :: END
% endif + % endif
@@ -507,7 +521,20 @@ DOCUMENTATION :: END % if data: -% if data['type'] == 'show' or data['type'] == 'artist': +% if data['type'] == 'library': + +% elif data['type'] == 'show' or data['type'] == 'artist': % endif -% if data['rating']: +% if data['type'] != 'library' and data['rating']: