diff --git a/.github/workflows/issues-stale.yml b/.github/workflows/issues-stale.yml index 0643cb0a..26b8aa5f 100644 --- a/.github/workflows/issues-stale.yml +++ b/.github/workflows/issues-stale.yml @@ -10,7 +10,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Stale - uses: actions/stale@v7 + uses: actions/stale@v8 with: stale-issue-message: > This issue is stale because it has been open for 30 days with no activity. @@ -30,7 +30,7 @@ jobs: days-before-close: 5 - name: Invalid Template - uses: actions/stale@v7 + uses: actions/stale@v8 with: stale-issue-message: > Invalid issues template. diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index 6480575f..6d91bbf6 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -95,7 +95,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status diff --git a/.github/workflows/publish-installers.yml b/.github/workflows/publish-installers.yml index 49d53233..0b6eec36 100644 --- a/.github/workflows/publish-installers.yml +++ b/.github/workflows/publish-installers.yml @@ -68,7 +68,7 @@ jobs: pyinstaller -y ./package/Tautulli-${{ matrix.os }}.spec - name: Create Windows Installer - uses: joncloud/makensis-action@v3.7 + uses: joncloud/makensis-action@v4 if: matrix.os == 'windows' with: script-file: ./package/Tautulli.nsi @@ -100,10 +100,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Checkout Code - uses: actions/checkout@v3.2.0 + uses: actions/checkout@v3 - name: Set Release Version id: get_version @@ -168,7 +168,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status diff --git a/.github/workflows/publish-snap.yml b/.github/workflows/publish-snap.yml index 9df4d2fd..dd74c3a3 100644 --- a/.github/workflows/publish-snap.yml +++ b/.github/workflows/publish-snap.yml @@ -70,7 +70,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Get Build Job Status - uses: technote-space/workflow-conclusion-action@v3.0 + uses: technote-space/workflow-conclusion-action@v3 - name: Combine Job Status id: status diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 58cb4ee4..1a24cf24 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -18,7 +18,6 @@ jobs: with: message: Pull requests must be made to the `nightly` branch. Thanks. repo-token: ${{ secrets.GITHUB_TOKEN }} - repo-token-user-login: 'github-actions[bot]' - name: Fail Workflow if: github.base_ref != 'nightly' diff --git a/CHANGELOG.md b/CHANGELOG.md index 24baf072..974e69ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## v2.12.5 (2023-07-13) + +* Activity: + * New: Added d3d11va to list of hardware decoders. +* History: + * Fix: Incorrect grouping of play history. + * New: Added button in settings to regroup play history. +* Notifications: + * Fix: Incorrect concurrent streams notifications by IP addresss for IPv6 addresses (#2096) (Thanks @pooley182) +* UI: + * Fix: Occasional UI crashing on Python 3.11. + * New: Added multiselect user filters to History and Graphs pages. (#2090) (Thanks @zdimension) +* API: + * New: Added regroup_history API command. + * Change: Updated graph API commands to accept a comma separated list of user IDs. + + ## v2.12.4 (2023-05-23) * History: diff --git a/data/interfaces/default/css/bootstrap-select.min.css b/data/interfaces/default/css/bootstrap-select.min.css new file mode 100644 index 00000000..d22faa63 --- /dev/null +++ b/data/interfaces/default/css/bootstrap-select.min.css @@ -0,0 +1,6 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */@-webkit-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@-o-keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}@keyframes bs-notify-fadeOut{0%{opacity:.9}100%{opacity:0}}.bootstrap-select>select.bs-select-hidden,select.bs-select-hidden,select.selectpicker{display:none!important}.bootstrap-select{width:220px\0;vertical-align:middle}.bootstrap-select>.dropdown-toggle{position:relative;width:100%;text-align:right;white-space:nowrap;display:-webkit-inline-box;display:-webkit-inline-flex;display:-ms-inline-flexbox;display:inline-flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.bootstrap-select>.dropdown-toggle:after{margin-top:-1px}.bootstrap-select>.dropdown-toggle.bs-placeholder,.bootstrap-select>.dropdown-toggle.bs-placeholder:active,.bootstrap-select>.dropdown-toggle.bs-placeholder:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder:hover{color:#999}.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-danger:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-dark:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-info:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-primary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-secondary:hover,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:active,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:focus,.bootstrap-select>.dropdown-toggle.bs-placeholder.btn-success:hover{color:rgba(255,255,255,.5)}.bootstrap-select>select{position:absolute!important;bottom:0;left:50%;display:block!important;width:.5px!important;height:100%!important;padding:0!important;opacity:0!important;border:none;z-index:0!important}.bootstrap-select>select.mobile-device{top:0;left:0;display:block!important;width:100%!important;z-index:2!important}.bootstrap-select.is-invalid .dropdown-toggle,.error .bootstrap-select .dropdown-toggle,.has-error .bootstrap-select .dropdown-toggle,.was-validated .bootstrap-select select:invalid+.dropdown-toggle{border-color:#b94a48}.bootstrap-select.is-valid .dropdown-toggle,.was-validated .bootstrap-select select:valid+.dropdown-toggle{border-color:#28a745}.bootstrap-select.fit-width{width:auto!important}.bootstrap-select:not([class*=col-]):not([class*=form-control]):not(.input-group-btn){width:220px}.bootstrap-select .dropdown-toggle:focus,.bootstrap-select>select.mobile-device:focus+.dropdown-toggle{outline:thin dotted #333!important;outline:5px auto -webkit-focus-ring-color!important;outline-offset:-2px}.bootstrap-select.form-control{margin-bottom:0;padding:0;border:none;height:auto}:not(.input-group)>.bootstrap-select.form-control:not([class*=col-]){width:100%}.bootstrap-select.form-control.input-group-btn{float:none;z-index:auto}.form-inline .bootstrap-select,.form-inline .bootstrap-select.form-control:not([class*=col-]){width:auto}.bootstrap-select:not(.input-group-btn),.bootstrap-select[class*=col-]{float:none;display:inline-block;margin-left:0}.bootstrap-select.dropdown-menu-right,.bootstrap-select[class*=col-].dropdown-menu-right,.row .bootstrap-select[class*=col-].dropdown-menu-right{float:right}.form-group .bootstrap-select,.form-horizontal .bootstrap-select,.form-inline .bootstrap-select{margin-bottom:0}.form-group-lg .bootstrap-select.form-control,.form-group-sm .bootstrap-select.form-control{padding:0}.form-group-lg .bootstrap-select.form-control .dropdown-toggle,.form-group-sm .bootstrap-select.form-control .dropdown-toggle{height:100%;font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-lg .dropdown-toggle,.bootstrap-select.form-control-sm .dropdown-toggle{font-size:inherit;line-height:inherit;border-radius:inherit}.bootstrap-select.form-control-sm .dropdown-toggle{padding:.25rem .5rem}.bootstrap-select.form-control-lg .dropdown-toggle{padding:.5rem 1rem}.form-inline .bootstrap-select .form-control{width:100%}.bootstrap-select.disabled,.bootstrap-select>.disabled{cursor:not-allowed}.bootstrap-select.disabled:focus,.bootstrap-select>.disabled:focus{outline:0!important}.bootstrap-select.bs-container{position:absolute;top:0;left:0;height:0!important;padding:0!important}.bootstrap-select.bs-container .dropdown-menu{z-index:1060}.bootstrap-select .dropdown-toggle .filter-option{position:static;top:0;left:0;float:left;height:100%;width:100%;text-align:left;overflow:hidden;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto}.bs3.bootstrap-select .dropdown-toggle .filter-option{padding-right:inherit}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option{position:absolute;padding-top:inherit;padding-bottom:inherit;padding-left:inherit;float:none}.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner{padding-right:inherit}.bootstrap-select .dropdown-toggle .filter-option-inner-inner{overflow:hidden}.bootstrap-select .dropdown-toggle .filter-expand{width:0!important;float:left;opacity:0!important;overflow:hidden}.bootstrap-select .dropdown-toggle .caret{position:absolute;top:50%;right:12px;margin-top:-2px;vertical-align:middle}.input-group .bootstrap-select.form-control .dropdown-toggle{border-radius:inherit}.bootstrap-select[class*=col-] .dropdown-toggle{width:100%}.bootstrap-select .dropdown-menu{min-width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu>.inner:focus{outline:0!important}.bootstrap-select .dropdown-menu.inner{position:static;float:none;border:0;padding:0;margin:0;border-radius:0;-webkit-box-shadow:none;box-shadow:none}.bootstrap-select .dropdown-menu li{position:relative}.bootstrap-select .dropdown-menu li.active small{color:rgba(255,255,255,.5)!important}.bootstrap-select .dropdown-menu li.disabled a{cursor:not-allowed}.bootstrap-select .dropdown-menu li a{cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.bootstrap-select .dropdown-menu li a.opt{position:relative;padding-left:2.25em}.bootstrap-select .dropdown-menu li a span.check-mark{display:none}.bootstrap-select .dropdown-menu li a span.text{display:inline-block}.bootstrap-select .dropdown-menu li small{padding-left:.5em}.bootstrap-select .dropdown-menu .notify{position:absolute;bottom:5px;width:96%;margin:0 2%;min-height:26px;padding:3px 5px;background:#f5f5f5;border:1px solid #e3e3e3;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,.05);box-shadow:inset 0 1px 1px rgba(0,0,0,.05);pointer-events:none;opacity:.9;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-select .dropdown-menu .notify.fadeOut{-webkit-animation:.3s linear 750ms forwards bs-notify-fadeOut;-o-animation:.3s linear 750ms forwards bs-notify-fadeOut;animation:.3s linear 750ms forwards bs-notify-fadeOut}.bootstrap-select .no-results{padding:3px;background:#f5f5f5;margin:0 5px;white-space:nowrap}.bootstrap-select.fit-width .dropdown-toggle .filter-option{position:static;display:inline;padding:0}.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner,.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner{display:inline}.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before{content:'\00a0'}.bootstrap-select.fit-width .dropdown-toggle .caret{position:static;top:auto;margin-top:-1px}.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark{position:absolute;display:inline-block;right:15px;top:5px}.bootstrap-select.show-tick .dropdown-menu li a span.text{margin-right:34px}.bootstrap-select .bs-ok-default:after{content:'';display:block;width:.5em;height:1em;border-style:solid;border-width:0 .26em .26em 0;-webkit-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg);transform:rotate(45deg)}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle{z-index:1061}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before{content:'';border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid rgba(204,204,204,.2);position:absolute;bottom:-4px;left:9px;display:none}.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after{content:'';border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;bottom:-4px;left:10px;display:none}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before{bottom:auto;top:-4px;border-top:7px solid rgba(204,204,204,.2);border-bottom:0}.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after{bottom:auto;top:-4px;border-top:6px solid #fff;border-bottom:0}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before{right:12px;left:auto}.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after{right:13px;left:auto}.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.open>.dropdown-toggle .filter-option:before,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:after,.bootstrap-select.show-menu-arrow.show>.dropdown-toggle .filter-option:before{display:block}.bs-actionsbox,.bs-donebutton,.bs-searchbox{padding:4px 8px}.bs-actionsbox{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-actionsbox .btn-group button{width:50%}.bs-donebutton{float:left;width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bs-donebutton .btn-group button{width:100%}.bs-searchbox+.bs-actionsbox{padding:0 8px 4px}.bs-searchbox .form-control{margin-bottom:0;width:100%;float:none} \ No newline at end of file diff --git a/data/interfaces/default/css/tautulli.css b/data/interfaces/default/css/tautulli.css index 5f1d90a0..e256d2d7 100644 --- a/data/interfaces/default/css/tautulli.css +++ b/data/interfaces/default/css/tautulli.css @@ -2914,7 +2914,7 @@ a .home-platforms-list-cover-face:hover margin-bottom: -20px; width: 100%; max-width: 1750px; - overflow: hidden; + display: flow-root; } .table-card-back td { font-size: 12px; diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html index 3f189112..8435df20 100644 --- a/data/interfaces/default/graphs.html +++ b/data/interfaces/default/graphs.html @@ -1,6 +1,7 @@ <%inherit file="base.html"/> <%def name="headIncludes()"> + @@ -14,9 +15,7 @@
@@ -225,6 +224,7 @@ <%def name="javascriptIncludes()"> + @@ -373,14 +373,35 @@ type: 'get', dataType: "json", success: function (data) { - var select = $('#graph-user'); + let select = $('#graph-user'); + let by_id = {}; data.sort(function(a, b) { return a.friendly_name.localeCompare(b.friendly_name); }); data.forEach(function(item) { select.append(''); + by_id[item.user_id] = item.friendly_name; }); + select.selectpicker({ + countSelectedText: function(sel, total) { + if (sel === 0 || sel === total) { + return 'All users'; + } else if (sel > 1) { + return sel + ' users'; + } else { + return select.val().map(function(id) { + return by_id[id]; + }).join(', '); + } + }, + style: 'btn-dark', + actionsBox: true, + selectedTextFormat: 'count', + noneSelectedText: 'All users' + }); + select.selectpicker('render'); + select.selectpicker('selectAll'); } }); @@ -602,11 +623,6 @@ $('#nav-tabs-total').tab('show'); } - // Set initial state - if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); } - if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); } - if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); } - // Tab1 opened $('#nav-tabs-plays').on('shown.bs.tab', function (e) { e.preventDefault(); @@ -652,9 +668,20 @@ $('.months').text(current_month_range); }); + let graph_user_last_id = undefined; + // User changed $('#graph-user').on('change', function() { - selected_user_id = $(this).val() || null; + let val = $(this).val(); + if (val.length === 0 || val.length === $(this).children().length) { + selected_user_id = null; // if all users are selected, just send an empty list + } else { + selected_user_id = val.join(","); + } + if (selected_user_id === graph_user_last_id) { + return; + } + graph_user_last_id = selected_user_id; if (current_tab === '#tabs-plays') { loadGraphsTab1(current_day_range, yaxis); } if (current_tab === '#tabs-stream') { loadGraphsTab2(current_day_range, yaxis); } if (current_tab === '#tabs-total') { loadGraphsTab3(current_month_range, yaxis); } diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index 327b99b7..8ab8b19e 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -1,6 +1,7 @@ <%inherit file="base.html"/> <%def name="headIncludes()"> + @@ -31,9 +32,7 @@ % if _session['user_group'] == 'admin':
@@ -121,6 +120,7 @@ <%def name="javascriptIncludes()"> + @@ -134,17 +134,40 @@ type: 'GET', dataType: 'json', success: function (data) { - var select = $('#history-user'); + let select = $('#history-user'); + let by_id = {}; data.sort(function (a, b) { return a.friendly_name.localeCompare(b.friendly_name); }); data.forEach(function (item) { select.append(''); + by_id[item.user_id] = item.friendly_name; }); + select.selectpicker({ + countSelectedText: function(sel, total) { + if (sel === 0 || sel === total) { + return 'All users'; + } else if (sel > 1) { + return sel + ' users'; + } else { + return select.val().map(function(id) { + return by_id[id]; + }).join(', '); + } + }, + style: 'btn-dark', + actionsBox: true, + selectedTextFormat: 'count', + noneSelectedText: 'All users' + }); + select.selectpicker('render'); + select.selectpicker('selectAll'); } }); + let history_user_last_id = undefined; + function loadHistoryTable(media_type, transcode_decision, selected_user_id) { history_table_options.ajax = { url: 'get_history', @@ -187,7 +210,16 @@ }); $('#history-user').on('change', function () { - selected_user_id = $(this).val() || null; + let val = $(this).val(); + if (val.length === 0 || val.length === $(this).children().length) { + selected_user_id = null; // if all users are selected, just send an empty list + } else { + selected_user_id = val.join(","); + } + if (selected_user_id === history_user_last_id) { + return; + } + history_user_last_id = selected_user_id; history_table.draw(); }); } diff --git a/data/interfaces/default/js/bootstrap-select.min.js b/data/interfaces/default/js/bootstrap-select.min.js new file mode 100644 index 00000000..92e3a32e --- /dev/null +++ b/data/interfaces/default/js/bootstrap-select.min.js @@ -0,0 +1,9 @@ +/*! + * Bootstrap-select v1.13.14 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2020 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +!function(e,t){void 0===e&&void 0!==window&&(e=window),"function"==typeof define&&define.amd?define(["jquery"],function(e){return t(e)}):"object"==typeof module&&module.exports?module.exports=t(require("jquery")):t(e.jQuery)}(this,function(e){!function(z){"use strict";var d=["sanitize","whiteList","sanitizeFn"],r=["background","cite","href","itemtype","longdesc","poster","src","xlink:href"],e={"*":["class","dir","id","lang","role","tabindex","style",/^aria-[\w-]*$/i],a:["target","href","title","rel"],area:[],b:[],br:[],col:[],code:[],div:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:["src","alt","title","width","height"],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},l=/^(?:(?:https?|mailto|ftp|tel|file):|[^&:/?#]*(?:[/?#]|$))/gi,a=/^data:(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm)|audio\/(?:mp3|oga|ogg|opus));base64,[a-z0-9+/]+=*$/i;function v(e,t){var i=e.nodeName.toLowerCase();if(-1!==z.inArray(i,t))return-1===z.inArray(i,r)||Boolean(e.nodeValue.match(l)||e.nodeValue.match(a));for(var s=z(t).filter(function(e,t){return t instanceof RegExp}),n=0,o=s.length;n]+>/g,"")),s&&(a=w(a)),a=a.toUpperCase(),o="contains"===i?0<=a.indexOf(t):a.startsWith(t)))break}return o}function L(e){return parseInt(e,10)||0}z.fn.triggerNative=function(e){var t,i=this[0];i.dispatchEvent?(u?t=new Event(e,{bubbles:!0}):(t=document.createEvent("Event")).initEvent(e,!0,!1),i.dispatchEvent(t)):i.fireEvent?((t=document.createEventObject()).eventType=e,i.fireEvent("on"+e,t)):this.trigger(e)};var f={"\xc0":"A","\xc1":"A","\xc2":"A","\xc3":"A","\xc4":"A","\xc5":"A","\xe0":"a","\xe1":"a","\xe2":"a","\xe3":"a","\xe4":"a","\xe5":"a","\xc7":"C","\xe7":"c","\xd0":"D","\xf0":"d","\xc8":"E","\xc9":"E","\xca":"E","\xcb":"E","\xe8":"e","\xe9":"e","\xea":"e","\xeb":"e","\xcc":"I","\xcd":"I","\xce":"I","\xcf":"I","\xec":"i","\xed":"i","\xee":"i","\xef":"i","\xd1":"N","\xf1":"n","\xd2":"O","\xd3":"O","\xd4":"O","\xd5":"O","\xd6":"O","\xd8":"O","\xf2":"o","\xf3":"o","\xf4":"o","\xf5":"o","\xf6":"o","\xf8":"o","\xd9":"U","\xda":"U","\xdb":"U","\xdc":"U","\xf9":"u","\xfa":"u","\xfb":"u","\xfc":"u","\xdd":"Y","\xfd":"y","\xff":"y","\xc6":"Ae","\xe6":"ae","\xde":"Th","\xfe":"th","\xdf":"ss","\u0100":"A","\u0102":"A","\u0104":"A","\u0101":"a","\u0103":"a","\u0105":"a","\u0106":"C","\u0108":"C","\u010a":"C","\u010c":"C","\u0107":"c","\u0109":"c","\u010b":"c","\u010d":"c","\u010e":"D","\u0110":"D","\u010f":"d","\u0111":"d","\u0112":"E","\u0114":"E","\u0116":"E","\u0118":"E","\u011a":"E","\u0113":"e","\u0115":"e","\u0117":"e","\u0119":"e","\u011b":"e","\u011c":"G","\u011e":"G","\u0120":"G","\u0122":"G","\u011d":"g","\u011f":"g","\u0121":"g","\u0123":"g","\u0124":"H","\u0126":"H","\u0125":"h","\u0127":"h","\u0128":"I","\u012a":"I","\u012c":"I","\u012e":"I","\u0130":"I","\u0129":"i","\u012b":"i","\u012d":"i","\u012f":"i","\u0131":"i","\u0134":"J","\u0135":"j","\u0136":"K","\u0137":"k","\u0138":"k","\u0139":"L","\u013b":"L","\u013d":"L","\u013f":"L","\u0141":"L","\u013a":"l","\u013c":"l","\u013e":"l","\u0140":"l","\u0142":"l","\u0143":"N","\u0145":"N","\u0147":"N","\u014a":"N","\u0144":"n","\u0146":"n","\u0148":"n","\u014b":"n","\u014c":"O","\u014e":"O","\u0150":"O","\u014d":"o","\u014f":"o","\u0151":"o","\u0154":"R","\u0156":"R","\u0158":"R","\u0155":"r","\u0157":"r","\u0159":"r","\u015a":"S","\u015c":"S","\u015e":"S","\u0160":"S","\u015b":"s","\u015d":"s","\u015f":"s","\u0161":"s","\u0162":"T","\u0164":"T","\u0166":"T","\u0163":"t","\u0165":"t","\u0167":"t","\u0168":"U","\u016a":"U","\u016c":"U","\u016e":"U","\u0170":"U","\u0172":"U","\u0169":"u","\u016b":"u","\u016d":"u","\u016f":"u","\u0171":"u","\u0173":"u","\u0174":"W","\u0175":"w","\u0176":"Y","\u0177":"y","\u0178":"Y","\u0179":"Z","\u017b":"Z","\u017d":"Z","\u017a":"z","\u017c":"z","\u017e":"z","\u0132":"IJ","\u0133":"ij","\u0152":"Oe","\u0153":"oe","\u0149":"'n","\u017f":"s"},m=/[\xc0-\xd6\xd8-\xf6\xf8-\xff\u0100-\u017f]/g,g=RegExp("[\\u0300-\\u036f\\ufe20-\\ufe2f\\u20d0-\\u20ff\\u1ab0-\\u1aff\\u1dc0-\\u1dff]","g");function b(e){return f[e]}function w(e){return(e=e.toString())&&e.replace(m,b).replace(g,"")}var I,x,y,$,S=(I={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},x="(?:"+Object.keys(I).join("|")+")",y=RegExp(x),$=RegExp(x,"g"),function(e){return e=null==e?"":""+e,y.test(e)?e.replace($,E):e});function E(e){return I[e]}var C={32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",65:"A",66:"B",67:"C",68:"D",69:"E",70:"F",71:"G",72:"H",73:"I",74:"J",75:"K",76:"L",77:"M",78:"N",79:"O",80:"P",81:"Q",82:"R",83:"S",84:"T",85:"U",86:"V",87:"W",88:"X",89:"Y",90:"Z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9"},N=27,D=13,H=32,W=9,B=38,M=40,R={success:!1,major:"3"};try{R.full=(z.fn.dropdown.Constructor.VERSION||"").split(" ")[0].split("."),R.major=R.full[0],R.success=!0}catch(e){}var U=0,j=".bs.select",V={DISABLED:"disabled",DIVIDER:"divider",SHOW:"open",DROPUP:"dropup",MENU:"dropdown-menu",MENURIGHT:"dropdown-menu-right",MENULEFT:"dropdown-menu-left",BUTTONCLASS:"btn-default",POPOVERHEADER:"popover-title",ICONBASE:"glyphicon",TICKICON:"glyphicon-ok"},F={MENU:"."+V.MENU},_={span:document.createElement("span"),i:document.createElement("i"),subtext:document.createElement("small"),a:document.createElement("a"),li:document.createElement("li"),whitespace:document.createTextNode("\xa0"),fragment:document.createDocumentFragment()};_.a.setAttribute("role","option"),"4"===R.major&&(_.a.className="dropdown-item"),_.subtext.className="text-muted",_.text=_.span.cloneNode(!1),_.text.className="text",_.checkMark=_.span.cloneNode(!1);var G=new RegExp(B+"|"+M),q=new RegExp("^"+W+"$|"+N),K={li:function(e,t,i){var s=_.li.cloneNode(!1);return e&&(1===e.nodeType||11===e.nodeType?s.appendChild(e):s.innerHTML=e),void 0!==t&&""!==t&&(s.className=t),null!=i&&s.classList.add("optgroup-"+i),s},a:function(e,t,i){var s=_.a.cloneNode(!0);return e&&(11===e.nodeType?s.appendChild(e):s.insertAdjacentHTML("beforeend",e)),void 0!==t&&""!==t&&s.classList.add.apply(s.classList,t.split(" ")),i&&s.setAttribute("style",i),s},text:function(e,t){var i,s,n=_.text.cloneNode(!1);if(e.content)n.innerHTML=e.content;else{if(n.textContent=e.text,e.icon){var o=_.whitespace.cloneNode(!1);(s=(!0===t?_.i:_.span).cloneNode(!1)).className=this.options.iconBase+" "+e.icon,_.fragment.appendChild(s),_.fragment.appendChild(o)}e.subtext&&((i=_.subtext.cloneNode(!1)).textContent=e.subtext,n.appendChild(i))}if(!0===t)for(;0'},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0,virtualScroll:600,display:!1,sanitize:!0,sanitizeFn:null,whiteList:e},Y.prototype={constructor:Y,init:function(){var i=this,e=this.$element.attr("id");U++,this.selectId="bs-select-"+U,this.$element[0].classList.add("bs-select-hidden"),this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$element[0].classList.contains("show-tick")&&(this.options.showTick=!0),this.$newElement=this.createDropdown(),this.buildData(),this.$element.after(this.$newElement).prependTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(F.MENU),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element[0].classList.remove("bs-select-hidden"),!0===this.options.dropdownAlignRight&&this.$menu[0].classList.add(V.MENURIGHT),void 0!==e&&this.$button.attr("data-id",e),this.checkDisabled(),this.clickListener(),this.options.liveSearch?(this.liveSearchListener(),this.focusedParent=this.$searchbox[0]):this.focusedParent=this.$menuInner[0],this.setStyle(),this.render(),this.setWidth(),this.options.container?this.selectPosition():this.$element.on("hide"+j,function(){if(i.isVirtual()){var e=i.$menuInner[0],t=e.firstChild.cloneNode(!1);e.replaceChild(t,e.firstChild),e.scrollTop=0}}),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(e){i.$element.trigger("hide"+j,e)},"hidden.bs.dropdown":function(e){i.$element.trigger("hidden"+j,e)},"show.bs.dropdown":function(e){i.$element.trigger("show"+j,e)},"shown.bs.dropdown":function(e){i.$element.trigger("shown"+j,e)}}),i.$element[0].hasAttribute("required")&&this.$element.on("invalid"+j,function(){i.$button[0].classList.add("bs-invalid"),i.$element.on("shown"+j+".invalid",function(){i.$element.val(i.$element.val()).off("shown"+j+".invalid")}).on("rendered"+j,function(){this.validity.valid&&i.$button[0].classList.remove("bs-invalid"),i.$element.off("rendered"+j)}),i.$button.on("blur"+j,function(){i.$element.trigger("focus").trigger("blur"),i.$button.off("blur"+j)})}),setTimeout(function(){i.buildList(),i.$element.trigger("loaded"+j)})},createDropdown:function(){var e=this.multiple||this.options.showTick?" show-tick":"",t=this.multiple?' aria-multiselectable="true"':"",i="",s=this.autofocus?" autofocus":"";R.major<4&&this.$element.parent().hasClass("input-group")&&(i=" input-group-btn");var n,o="",r="",l="",a="";return this.options.header&&(o='
'+this.options.header+"
"),this.options.liveSearch&&(r=''),this.multiple&&this.options.actionsBox&&(l='
"),this.multiple&&this.options.doneButton&&(a='
"),n='",z(n)},setPositionData:function(){this.selectpicker.view.canHighlight=[];for(var e=this.selectpicker.view.size=0;e=this.options.virtualScroll||!0===this.options.virtualScroll},createView:function(A,e,t){var L,N,D=this,i=0,H=[];if(this.selectpicker.isSearching=A,this.selectpicker.current=A?this.selectpicker.search:this.selectpicker.main,this.setPositionData(),e)if(t)i=this.$menuInner[0].scrollTop;else if(!D.multiple){var s=D.$element[0],n=(s.options[s.selectedIndex]||{}).liIndex;if("number"==typeof n&&!1!==D.options.size){var o=D.selectpicker.main.data[n],r=o&&o.position;r&&(i=r-(D.sizeInfo.menuInnerHeight+D.sizeInfo.liHeight)/2)}}function l(e,t){var i,s,n,o,r,l,a,c,d=D.selectpicker.current.elements.length,h=[],p=!0,u=D.isVirtual();D.selectpicker.view.scrollTop=e,i=Math.ceil(D.sizeInfo.menuInnerHeight/D.sizeInfo.liHeight*1.5),s=Math.round(d/i)||1;for(var f=0;fd-1?0:D.selectpicker.current.data[d-1].position-D.selectpicker.current.data[D.selectpicker.view.position1-1].position,b.firstChild.style.marginTop=v+"px",b.firstChild.style.marginBottom=g+"px"):(b.firstChild.style.marginTop=0,b.firstChild.style.marginBottom=0),b.firstChild.appendChild(w),!0===u&&D.sizeInfo.hasScrollBar){var C=b.firstChild.offsetWidth;if(t&&CD.sizeInfo.selectWidth)b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px";else if(C>D.sizeInfo.menuInnerInnerWidth){D.$menu[0].style.minWidth=0;var O=b.firstChild.offsetWidth;O>D.sizeInfo.menuInnerInnerWidth&&(D.sizeInfo.menuInnerInnerWidth=O,b.firstChild.style.minWidth=D.sizeInfo.menuInnerInnerWidth+"px"),D.$menu[0].style.minWidth=""}}}if(D.prevActiveIndex=D.activeIndex,D.options.liveSearch){if(A&&t){var z,T=0;D.selectpicker.view.canHighlight[T]||(T=1+D.selectpicker.view.canHighlight.slice(1).indexOf(!0)),z=D.selectpicker.view.visibleElements[T],D.defocusItem(D.selectpicker.view.currentActive),D.activeIndex=(D.selectpicker.current.data[T]||{}).index,D.focusItem(z)}}else D.$menuInner.trigger("focus")}l(i,!0),this.$menuInner.off("scroll.createView").on("scroll.createView",function(e,t){D.noScroll||l(this.scrollTop,t),D.noScroll=!1}),z(window).off("resize"+j+"."+this.selectId+".createView").on("resize"+j+"."+this.selectId+".createView",function(){D.$newElement.hasClass(V.SHOW)&&l(D.$menuInner[0].scrollTop)})},focusItem:function(e,t,i){if(e){t=t||this.selectpicker.main.data[this.activeIndex];var s=e.firstChild;s&&(s.setAttribute("aria-setsize",this.selectpicker.view.size),s.setAttribute("aria-posinset",t.posinset),!0!==i&&(this.focusedParent.setAttribute("aria-activedescendant",s.id),e.classList.add("active"),s.classList.add("active")))}},defocusItem:function(e){e&&(e.classList.remove("active"),e.firstChild&&e.firstChild.classList.remove("active"))},setPlaceholder:function(){var e=!1;if(this.options.title&&!this.multiple){this.selectpicker.view.titleOption||(this.selectpicker.view.titleOption=document.createElement("option")),e=!0;var t=this.$element[0],i=!1,s=!this.selectpicker.view.titleOption.parentNode;if(s)this.selectpicker.view.titleOption.className="bs-title-option",this.selectpicker.view.titleOption.value="",i=void 0===z(t.options[t.selectedIndex]).attr("selected")&&void 0===this.$element.data("selected");!s&&0===this.selectpicker.view.titleOption.index||t.insertBefore(this.selectpicker.view.titleOption,t.firstChild),i&&(t.selectedIndex=0)}return e},buildData:function(){var p=':not([hidden]):not([data-hidden="true"])',u=[],f=0,e=this.setPlaceholder()?1:0;this.options.hideDisabled&&(p+=":not(:disabled)");var t=this.$element[0].querySelectorAll("select > *"+p);function m(e){var t=u[u.length-1];t&&"divider"===t.type&&(t.optID||e.optID)||((e=e||{}).type="divider",u.push(e))}function v(e,t){if((t=t||{}).divider="true"===e.getAttribute("data-divider"),t.divider)m({optID:t.optID});else{var i=u.length,s=e.style.cssText,n=s?S(s):"",o=(e.className||"")+(t.optgroupClass||"");t.optID&&(o="opt "+o),t.optionClass=o.trim(),t.inlineStyle=n,t.text=e.textContent,t.content=e.getAttribute("data-content"),t.tokens=e.getAttribute("data-tokens"),t.subtext=e.getAttribute("data-subtext"),t.icon=e.getAttribute("data-icon"),e.liIndex=i,t.display=t.content||t.text,t.type="option",t.index=i,t.option=e,t.selected=!!e.selected,t.disabled=t.disabled||!!e.disabled,u.push(t)}}function i(e,t){var i=t[e],s=t[e-1],n=t[e+1],o=i.querySelectorAll("option"+p);if(o.length){var r,l,a={display:S(i.label),subtext:i.getAttribute("data-subtext"),icon:i.getAttribute("data-icon"),type:"optgroup-label",optgroupClass:" "+(i.className||"")};f++,s&&m({optID:f}),a.optID=f,u.push(a);for(var c=0,d=o.length;c li")},render:function(){var e,t=this,i=this.$element[0],s=this.setPlaceholder()&&0===i.selectedIndex,n=O(i,this.options.hideDisabled),o=n.length,r=this.$button[0],l=r.querySelector(".filter-option-inner-inner"),a=document.createTextNode(this.options.multipleSeparator),c=_.fragment.cloneNode(!1),d=!1;if(r.classList.toggle("bs-placeholder",t.multiple?!o:!T(i,n)),this.tabIndex(),"static"===this.options.selectedTextFormat)c=K.text.call(this,{text:this.options.title},!0);else if(!1===(this.multiple&&-1!==this.options.selectedTextFormat.indexOf("count")&&1")).length&&o>e[1]||1===e.length&&2<=o))){if(!s){for(var h=0;h option"+m+", optgroup"+m+" option"+m).length,g="function"==typeof this.options.countSelectedText?this.options.countSelectedText(o,v):this.options.countSelectedText;c=K.text.call(this,{text:g.replace("{0}",o.toString()).replace("{1}",v.toString())},!0)}if(null==this.options.title&&(this.options.title=this.$element.attr("title")),c.childNodes.length||(c=K.text.call(this,{text:void 0!==this.options.title?this.options.title:this.options.noneSelectedText},!0)),r.title=c.textContent.replace(/<[^>]*>?/g,"").trim(),this.options.sanitize&&d&&P([c],t.options.whiteList,t.options.sanitizeFn),l.innerHTML="",l.appendChild(c),R.major<4&&this.$newElement[0].classList.contains("bs3-has-addon")){var b=r.querySelector(".filter-expand"),w=l.cloneNode(!0);w.className="filter-expand",b?r.replaceChild(w,b):r.appendChild(w)}this.$element.trigger("rendered"+j)},setStyle:function(e,t){var i,s=this.$button[0],n=this.$newElement[0],o=this.options.style.trim();this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,"")),R.major<4&&(n.classList.add("bs3"),n.parentNode.classList.contains("input-group")&&(n.previousElementSibling||n.nextElementSibling)&&(n.previousElementSibling||n.nextElementSibling).classList.contains("input-group-addon")&&n.classList.add("bs3-has-addon")),i=e?e.trim():o,"add"==t?i&&s.classList.add.apply(s.classList,i.split(" ")):"remove"==t?i&&s.classList.remove.apply(s.classList,i.split(" ")):(o&&s.classList.remove.apply(s.classList,o.split(" ")),i&&s.classList.add.apply(s.classList,i.split(" ")))},liHeight:function(e){if(e||!1!==this.options.size&&!Object.keys(this.sizeInfo).length){var t=document.createElement("div"),i=document.createElement("div"),s=document.createElement("div"),n=document.createElement("ul"),o=document.createElement("li"),r=document.createElement("li"),l=document.createElement("li"),a=document.createElement("a"),c=document.createElement("span"),d=this.options.header&&0this.sizeInfo.menuExtras.vert&&l+this.sizeInfo.menuExtras.vert+50>this.sizeInfo.selectOffsetBot,!0===this.selectpicker.isSearching&&(a=this.selectpicker.dropup),this.$newElement.toggleClass(V.DROPUP,a),this.selectpicker.dropup=a),"auto"===this.options.size)n=3this.options.size){for(var b=0;bthis.sizeInfo.menuInnerHeight&&(this.sizeInfo.hasScrollBar=!0,this.sizeInfo.totalMenuWidth=this.sizeInfo.menuWidth+this.sizeInfo.scrollBarWidth),"auto"===this.options.dropdownAlignRight&&this.$menu.toggleClass(V.MENURIGHT,this.sizeInfo.selectOffsetLeft>this.sizeInfo.selectOffsetRight&&this.sizeInfo.selectOffsetRightthis.options.size&&i.off("resize"+j+"."+this.selectId+".setMenuSize scroll"+j+"."+this.selectId+".setMenuSize")}this.createView(!1,!0,e)},setWidth:function(){var i=this;"auto"===this.options.width?requestAnimationFrame(function(){i.$menu.css("min-width","0"),i.$element.on("loaded"+j,function(){i.liHeight(),i.setMenuSize();var e=i.$newElement.clone().appendTo("body"),t=e.css("width","auto").children("button").outerWidth();e.remove(),i.sizeInfo.selectWidth=Math.max(i.sizeInfo.totalMenuWidth,t),i.$newElement.css("width",i.sizeInfo.selectWidth+"px")})}):"fit"===this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width","").addClass("fit-width")):this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width",this.options.width)):(this.$menu.css("min-width",""),this.$newElement.css("width","")),this.$newElement.hasClass("fit-width")&&"fit"!==this.options.width&&this.$newElement[0].classList.remove("fit-width")},selectPosition:function(){this.$bsContainer=z('
');function e(e){var t={},i=r.options.display||!!z.fn.dropdown.Constructor.Default&&z.fn.dropdown.Constructor.Default.display;r.$bsContainer.addClass(e.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass(V.DROPUP,e.hasClass(V.DROPUP)),s=e.offset(),l.is("body")?n={top:0,left:0}:((n=l.offset()).top+=parseInt(l.css("borderTopWidth"))-l.scrollTop(),n.left+=parseInt(l.css("borderLeftWidth"))-l.scrollLeft()),o=e.hasClass(V.DROPUP)?0:e[0].offsetHeight,(R.major<4||"static"===i)&&(t.top=s.top-n.top+o,t.left=s.left-n.left),t.width=e[0].offsetWidth,r.$bsContainer.css(t)}var s,n,o,r=this,l=z(this.options.container);this.$button.on("click.bs.dropdown.data-api",function(){r.isDisabled()||(e(r.$newElement),r.$bsContainer.appendTo(r.options.container).toggleClass(V.SHOW,!r.$button.hasClass(V.SHOW)).append(r.$menu))}),z(window).off("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId).on("resize"+j+"."+this.selectId+" scroll"+j+"."+this.selectId,function(){r.$newElement.hasClass(V.SHOW)&&e(r.$newElement)}),this.$element.on("hide"+j,function(){r.$menu.data("height",r.$menu.height()),r.$bsContainer.detach()})},setOptionStatus:function(e){var t=this;if(t.noScroll=!1,t.selectpicker.view.visibleElements&&t.selectpicker.view.visibleElements.length)for(var i=0;i
');y[2]&&($=$.replace("{var}",y[2][1"+$+"
")),d=!1,C.$element.trigger("maxReached"+j)),g&&w&&(E.append(z("
"+S+"
")),d=!1,C.$element.trigger("maxReachedGrp"+j)),setTimeout(function(){C.setSelected(r,!1)},10),E[0].classList.add("fadeOut"),setTimeout(function(){E.remove()},1050)}}}else c&&(c.selected=!1),h.selected=!0,C.setSelected(r,!0);!C.multiple||C.multiple&&1===C.options.maxOptions?C.$button.trigger("focus"):C.options.liveSearch&&C.$searchbox.trigger("focus"),d&&(!C.multiple&&a===s.selectedIndex||(A=[h.index,p.prop("selected"),l],C.$element.triggerNative("change")))}}),this.$menu.on("click","li."+V.DISABLED+" a, ."+V.POPOVERHEADER+", ."+V.POPOVERHEADER+" :not(.close)",function(e){e.currentTarget==this&&(e.preventDefault(),e.stopPropagation(),C.options.liveSearch&&!z(e.target).hasClass("close")?C.$searchbox.trigger("focus"):C.$button.trigger("focus"))}),this.$menuInner.on("click",".divider, .dropdown-header",function(e){e.preventDefault(),e.stopPropagation(),C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus")}),this.$menu.on("click","."+V.POPOVERHEADER+" .close",function(){C.$button.trigger("click")}),this.$searchbox.on("click",function(e){e.stopPropagation()}),this.$menu.on("click",".actions-btn",function(e){C.options.liveSearch?C.$searchbox.trigger("focus"):C.$button.trigger("focus"),e.preventDefault(),e.stopPropagation(),z(this).hasClass("bs-select-all")?C.selectAll():C.deselectAll()}),this.$element.on("change"+j,function(){C.render(),C.$element.trigger("changed"+j,A),A=null}).on("focus"+j,function(){C.options.mobile||C.$button.trigger("focus")})},liveSearchListener:function(){var u=this,f=document.createElement("li");this.$button.on("click.bs.dropdown.data-api",function(){u.$searchbox.val()&&u.$searchbox.val("")}),this.$searchbox.on("click.bs.dropdown.data-api focus.bs.dropdown.data-api touchend.bs.dropdown.data-api",function(e){e.stopPropagation()}),this.$searchbox.on("input propertychange",function(){var e=u.$searchbox.val();if(u.selectpicker.search.elements=[],u.selectpicker.search.data=[],e){var t=[],i=e.toUpperCase(),s={},n=[],o=u._searchStyle(),r=u.options.liveSearchNormalize;r&&(i=w(i));for(var l=0;l=a.selectpicker.view.canHighlight.length&&(t=0),a.selectpicker.view.canHighlight[t+f]||(t=t+1+a.selectpicker.view.canHighlight.slice(t+f+1).indexOf(!0))),e.preventDefault();var m=f+t;e.which===B?0===f&&t===c.length-1?(a.$menuInner[0].scrollTop=a.$menuInner[0].scrollHeight,m=a.selectpicker.current.elements.length-1):d=(o=(n=a.selectpicker.current.data[m]).position-n.height)u+a.sizeInfo.menuInnerHeight),s=a.selectpicker.main.elements[v],a.activeIndex=b[x],a.focusItem(s),s&&s.firstChild.focus(),d&&(a.$menuInner[0].scrollTop=o),r.trigger("focus")}}i&&(e.which===H&&!a.selectpicker.keydown.keyHistory||e.which===D||e.which===W&&a.options.selectOnTab)&&(e.which!==H&&e.preventDefault(),a.options.liveSearch&&e.which===H||(a.$menuInner.find(".active a").trigger("click",!0),r.trigger("focus"),a.options.liveSearch||(e.preventDefault(),z(document).data("spaceSelect",!0))))}},mobile:function(){this.$element[0].classList.add("mobile-device")},refresh:function(){var e=z.extend({},this.options,this.$element.data());this.options=e,this.checkDisabled(),this.setStyle(),this.render(),this.buildData(),this.buildList(),this.setWidth(),this.setSize(!0),this.$element.trigger("refreshed"+j)},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(j).removeData("selectpicker").removeClass("bs-select-hidden selectpicker"),z(window).off(j+"."+this.selectId)}};var J=z.fn.selectpicker;z.fn.selectpicker=Z,z.fn.selectpicker.Constructor=Y,z.fn.selectpicker.noConflict=function(){return z.fn.selectpicker=J,this};var Q=z.fn.dropdown.Constructor._dataApiKeydownHandler||z.fn.dropdown.Constructor.prototype.keydown;z(document).off("keydown.bs.dropdown.data-api").on("keydown.bs.dropdown.data-api",':not(.bootstrap-select) > [data-toggle="dropdown"]',Q).on("keydown.bs.dropdown.data-api",":not(.bootstrap-select) > .dropdown-menu",Q).on("keydown"+j,'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',Y.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle="dropdown"], .bootstrap-select [role="listbox"], .bootstrap-select .bs-searchbox input',function(e){e.stopPropagation()}),z(window).on("load"+j+".data-api",function(){z(".selectpicker").each(function(){var e=z(this);Z.call(e,e.data())})})}(e)}); +//# sourceMappingURL=bootstrap-select.min.js.map \ No newline at end of file diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index fd234da2..c5d8fe37 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -132,12 +132,6 @@

Change the "Play by day of week" graph to start on Monday. Default is start on Sunday.

-
- -

Group play history for the same item and user as a single entry when progress is less than the watched percent.

-

Decide whether to use end credits markers to determine the 'watched' state of video items. When markers are not available the selected threshold percentage will be used.

+
+ +

Group play history for the same item and user as a single entry when progress is less than the watched percent.

+
+
+ +

+ Fix grouping of play history in the database.
+

+
+
+
+ +
+
+
+

@@ -2484,6 +2497,12 @@ $(document).ready(function() { confirmAjaxCall(url, msg); }); + $("#regroup_history").click(function () { + var msg = 'Are you sure you want to regroup play history in the database?

This make take a long time for large databases.
Regrouping will continue in the background.
'; + var url = 'regroup_history'; + confirmAjaxCall(url, msg); + }); + $("#delete_temp_sessions").click(function () { var msg = 'Are you sure you want to flush the temporary sessions?

This will reset all currently active sessions.'; var url = 'delete_temp_sessions'; diff --git a/package/requirements-package.txt b/package/requirements-package.txt index 173eba56..064d2246 100644 --- a/package/requirements-package.txt +++ b/package/requirements-package.txt @@ -2,10 +2,10 @@ apscheduler==3.10.1 importlib-metadata==6.0.0 importlib-resources==5.12.0 pyinstaller==5.8.0 -pyopenssl==23.0.0 -pycryptodomex==3.17 +pyopenssl==23.2.0 +pycryptodomex==3.18.0 -pyobjc-core==9.0.1; platform_system == "Darwin" +pyobjc-core==9.2; platform_system == "Darwin" pyobjc-framework-Cocoa==9.2; platform_system == "Darwin" -pywin32==305; platform_system == "Windows" +pywin32==306; platform_system == "Windows" diff --git a/plexpy/activity_processor.py b/plexpy/activity_processor.py index 588e91ce..9115f332 100644 --- a/plexpy/activity_processor.py +++ b/plexpy/activity_processor.py @@ -326,70 +326,7 @@ class ActivityProcessor(object): # Get the last insert row id last_id = self.db.last_insert_id() - new_session = prev_session = None - watched = False - - if session['live']: - # Check if we should group the session, select the last guid from the user - query = "SELECT session_history.id, session_history_metadata.guid, session_history.reference_id " \ - "FROM session_history " \ - "JOIN session_history_metadata ON session_history.id == session_history_metadata.id " \ - "WHERE session_history.user_id = ? ORDER BY session_history.id DESC LIMIT 1 " - - args = [session['user_id']] - - result = self.db.select(query=query, args=args) - - if len(result) > 0: - new_session = {'id': last_id, - 'guid': metadata['guid'], - 'reference_id': last_id} - - prev_session = {'id': result[0]['id'], - 'guid': result[0]['guid'], - 'reference_id': result[0]['reference_id']} - - else: - # Check if we should group the session, select the last two rows from the user - query = "SELECT id, rating_key, view_offset, reference_id FROM session_history " \ - "WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 " - - args = [session['user_id'], session['rating_key']] - - result = self.db.select(query=query, args=args) - - if len(result) > 1: - new_session = {'id': result[0]['id'], - 'rating_key': result[0]['rating_key'], - 'view_offset': result[0]['view_offset'], - 'reference_id': result[0]['reference_id']} - - prev_session = {'id': result[1]['id'], - 'rating_key': result[1]['rating_key'], - 'view_offset': result[1]['view_offset'], - 'reference_id': result[1]['reference_id']} - - marker_first, marker_final = helpers.get_first_final_marker(metadata['markers']) - watched = helpers.check_watched( - session['media_type'], session['view_offset'], session['duration'], - marker_first, marker_final - ) - - query = "UPDATE session_history SET reference_id = ? WHERE id = ? " - - # If previous session view offset less than watched percent, - # and new session view offset is greater, - # then set the reference_id to the previous row, - # else set the reference_id to the new id - if prev_session is None and new_session is None: - args = [last_id, last_id] - elif watched and prev_session['view_offset'] <= new_session['view_offset'] or \ - session['live'] and prev_session['guid'] == new_session['guid']: - args = [prev_session['reference_id'], new_session['id']] - else: - args = [new_session['id'], new_session['id']] - - self.db.action(query=query, args=args) + self.group_history(last_id, session, metadata) # logger.debug("Tautulli ActivityProcessor :: Successfully written history item, last id for session_history is %s" # % last_id) @@ -546,6 +483,80 @@ class ActivityProcessor(object): # Return the session row id when the session is successfully written to the database return session['id'] + def group_history(self, last_id, session, metadata=None): + new_session = prev_session = None + prev_watched = None + + if session['live']: + # Check if we should group the session, select the last guid from the user + query = "SELECT session_history.id, session_history_metadata.guid, session_history.reference_id " \ + "FROM session_history " \ + "JOIN session_history_metadata ON session_history.id == session_history_metadata.id " \ + "WHERE session_history.id <= ? AND session_history.user_id = ? ORDER BY session_history.id DESC LIMIT 1 " + + args = [last_id, session['user_id']] + + result = self.db.select(query=query, args=args) + + if len(result) > 0: + new_session = {'id': last_id, + 'guid': metadata['guid'] if metadata else session['guid'], + 'reference_id': last_id} + + prev_session = {'id': result[0]['id'], + 'guid': result[0]['guid'], + 'reference_id': result[0]['reference_id']} + + else: + # Check if we should group the session, select the last two rows from the user + query = "SELECT id, rating_key, view_offset, reference_id FROM session_history " \ + "WHERE id <= ? AND user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 " + + args = [last_id, session['user_id'], session['rating_key']] + + result = self.db.select(query=query, args=args) + + if len(result) > 1: + new_session = {'id': result[0]['id'], + 'rating_key': result[0]['rating_key'], + 'view_offset': helpers.cast_to_int(result[0]['view_offset']), + 'reference_id': result[0]['reference_id']} + + prev_session = {'id': result[1]['id'], + 'rating_key': result[1]['rating_key'], + 'view_offset': helpers.cast_to_int(result[1]['view_offset']), + 'reference_id': result[1]['reference_id']} + + if metadata: + marker_first, marker_final = helpers.get_first_final_marker(metadata['markers']) + else: + marker_first = session['marker_credits_first'] + marker_final = session['marker_credits_final'] + + prev_watched = helpers.check_watched( + session['media_type'], prev_session['view_offset'], session['duration'], + marker_first, marker_final + ) + + query = "UPDATE session_history SET reference_id = ? WHERE id = ? " + + # If previous session view offset less than watched threshold, + # and new session view offset is greater, + # then set the reference_id to the previous row, + # else set the reference_id to the new id + if (prev_watched is False and prev_session['view_offset'] <= new_session['view_offset'] or + session['live'] and prev_session['guid'] == new_session['guid']): + if metadata: + logger.debug("Tautulli ActivityProcessor :: Grouping history for sessionKey %s", session['session_key']) + args = [prev_session['reference_id'], new_session['id']] + + else: + if metadata: + logger.debug("Tautulli ActivityProcessor :: Not grouping history for sessionKey %s", session['session_key']) + args = [last_id, last_id] + + self.db.action(query=query, args=args) + def get_sessions(self, user_id=None, ip_address=None): query = "SELECT * FROM sessions" args = [] @@ -695,3 +706,36 @@ class ActivityProcessor(object): "ORDER BY stopped DESC", [user_id, machine_id, media_type]) return int(started - last_session.get('stopped', 0) >= plexpy.CONFIG.NOTIFY_CONTINUED_SESSION_THRESHOLD) + + def regroup_history(self): + logger.info("Tautulli ActivityProcessor :: Creating database backup...") + if not database.make_backup(): + return False + + logger.info("Tautulli ActivityProcessor :: Regrouping session history...") + + query = ( + "SELECT * FROM session_history " + "JOIN session_history_metadata ON session_history.id = session_history_metadata.id" + ) + results = self.db.select(query) + count = len(results) + progress = 0 + + for i, session in enumerate(results, start=1): + if int(i / count * 10) > progress: + progress = int(i / count * 10) + logger.info("Tautulli ActivityProcessor :: Regrouping session history: %d%%", progress * 10) + + try: + self.group_history(session['id'], session) + except Exception as e: + logger.error("Tautulli ActivityProcessor :: Error regrouping session history: %s", e) + return False + + logger.info("Tautulli ActivityProcessor :: Regrouping session history complete.") + return True + + +def regroup_history(): + ActivityProcessor().regroup_history() diff --git a/plexpy/common.py b/plexpy/common.py index cf1180dc..889d3f73 100644 --- a/plexpy/common.py +++ b/plexpy/common.py @@ -216,6 +216,7 @@ AUDIO_QUALITY_PROFILES = { AUDIO_QUALITY_PROFILES = OrderedDict(sorted(list(AUDIO_QUALITY_PROFILES.items()), key=lambda k: k[0], reverse=True)) HW_DECODERS = [ + 'd3d11va', 'dxva2', 'videotoolbox', 'mediacodecndk', diff --git a/plexpy/config.py b/plexpy/config.py index 7b583d8d..6f2926d9 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -177,6 +177,7 @@ _CONFIG_DEFINITIONS = { 'NOTIFY_RECENTLY_ADDED_UPGRADE': (int, 'Monitoring', 0), 'NOTIFY_REMOTE_ACCESS_THRESHOLD': (int, 'Monitoring', 60), 'NOTIFY_CONCURRENT_BY_IP': (int, 'Monitoring', 0), + 'NOTIFY_CONCURRENT_IPV6_CIDR': (str, 'Monitoring', '/64'), 'NOTIFY_CONCURRENT_THRESHOLD': (int, 'Monitoring', 2), 'NOTIFY_NEW_DEVICE_INITIAL_ONLY': (int, 'Monitoring', 1), 'NOTIFY_SERVER_CONNECTION_THRESHOLD': (int, 'Monitoring', 60), @@ -536,7 +537,7 @@ class Config(object): Returns something from the ini unless it is a real property of the configuration object or is not all caps. """ - if not re.match(r'[A-Z_]+$', name): + if not re.match(r'[A-Z0-9_]+$', name): return super(Config, self).__getattr__(name) else: return self.check_setting(name) diff --git a/plexpy/graphs.py b/plexpy/graphs.py index 49dfee57..58a199c0 100644 --- a/plexpy/graphs.py +++ b/plexpy/graphs.py @@ -51,11 +51,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -171,11 +167,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = "AND session_history.user_id = %s " % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = "AND session_history.user_id = %s " % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -308,11 +300,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -427,11 +415,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 12 timestamp = arrow.get(helpers.timestamp()).shift(months=-time_range).floor('month').timestamp() - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -554,11 +538,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -653,11 +633,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -763,11 +739,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -860,11 +832,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -941,11 +909,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -1048,11 +1012,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -1128,11 +1088,7 @@ class Graphs(object): time_range = helpers.cast_to_int(time_range) or 30 timestamp = helpers.timestamp() - time_range * 24 * 60 * 60 - user_cond = '' - if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): - user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() - elif user_id and user_id.isdigit(): - user_cond = 'AND session_history.user_id = %s ' % user_id + user_cond = self._make_user_cond(user_id) if grouping is None: grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES @@ -1212,3 +1168,16 @@ class Graphs(object): 'series': [series_1_output, series_2_output, series_3_output]} return output + + def _make_user_cond(self, user_id): + """ + Expects user_id to be a comma-separated list of ints. + """ + user_cond = '' + if session.get_session_user_id() and user_id and user_id != str(session.get_session_user_id()): + user_cond = 'AND session_history.user_id = %s ' % session.get_session_user_id() + elif user_id: + user_ids = helpers.split_strip(user_id) + if all(id.isdigit() for id in user_ids): + user_cond = 'AND session_history.user_id IN (%s) ' % ','.join(user_ids) + return user_cond diff --git a/plexpy/helpers.py b/plexpy/helpers.py index 9cfb9c45..085dfc12 100644 --- a/plexpy/helpers.py +++ b/plexpy/helpers.py @@ -33,6 +33,7 @@ from functools import reduce, wraps import hashlib import imghdr from future.moves.itertools import islice, zip_longest +from ipaddress import ip_address, ip_network, IPv4Address import ipwhois import ipwhois.exceptions import ipwhois.utils @@ -1777,3 +1778,18 @@ def check_watched(media_type, view_offset, duration, marker_credits_first=None, def pms_name(): return plexpy.CONFIG.PMS_NAME_OVERRIDE or plexpy.CONFIG.PMS_NAME + + +def ip_type(ip: str) -> str: + try: + return "IPv4" if type(ip_address(ip)) is IPv4Address else "IPv6" + except ValueError: + return "Invalid" + + +def get_ipv6_network_address(ip: str) -> str: + cidr = "/64" + cidr_pattern = re.compile(r'^/(1([0-1]\d|2[0-8]))$|^/(\d\d)$|^/[1-9]$') + if cidr_pattern.match(plexpy.CONFIG.NOTIFY_CONCURRENT_IPV6_CIDR): + cidr = plexpy.CONFIG.NOTIFY_CONCURRENT_IPV6_CIDR + return str(ip_network(ip+cidr, strict=False).network_address) diff --git a/plexpy/newsletters.py b/plexpy/newsletters.py index 94f73c8f..661a2b42 100644 --- a/plexpy/newsletters.py +++ b/plexpy/newsletters.py @@ -318,7 +318,7 @@ def blacklist_logger(): logger.blacklist_config(email_config) -def serve_template(templatename, **kwargs): +def serve_template(template_name, **kwargs): if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR: logger.info("Tautulli Newsletters :: Using custom newsletter template directory.") template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR @@ -327,12 +327,12 @@ def serve_template(templatename, **kwargs): template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.NEWSLETTER_TEMPLATES) if not plexpy.CONFIG.NEWSLETTER_INLINE_STYLES: - templatename = templatename.replace('.html', '.internal.html') + template_name = template_name.replace('.html', '.internal.html') _hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h']) try: - template = _hplookup.get_template(templatename) + template = _hplookup.get_template(template_name) return template.render(**kwargs), False except: return exceptions.html_error_template().render(), True @@ -477,7 +477,7 @@ class Newsletter(object): logger.info("Tautulli Newsletters :: Generating newsletter%s." % (' preview' if self.is_preview else '')) newsletter_rendered, self.template_error = serve_template( - templatename=self._TEMPLATE, + template_name=self._TEMPLATE, uuid=self.uuid, subject=self.subject_formatted, body=self.body_formatted, diff --git a/plexpy/notification_handler.py b/plexpy/notification_handler.py index 7dd81627..8b4b8583 100644 --- a/plexpy/notification_handler.py +++ b/plexpy/notification_handler.py @@ -160,6 +160,7 @@ def add_notifier_each(notifier_id=None, notify_action=None, stream_data=None, ti def notify_conditions(notify_action=None, stream_data=None, timeline_data=None, **kwargs): logger.debug("Tautulli NotificationHandler :: Checking global notification conditions.") + evaluated = False # Activity notifications if stream_data: @@ -187,7 +188,13 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None, user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']] if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP: - evaluated = len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD + ip_addresses = set() + for s in user_sessions: + if helpers.ip_type(s['ip_address']) == 'IPv6': + ip_addresses.add(helpers.get_ipv6_network_address(s['ip_address'])) + elif helpers.ip_type(s['ip_address']) == 'IPv4': + ip_addresses.add(s['ip_address']) + evaluated = len(ip_addresses) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD else: evaluated = len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index a2fa6341..2611aaea 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -3967,6 +3967,14 @@ class TAUTULLIREMOTEAPP(Notifier): 2: 'Large image (Non-expandable text)' } }) + elif platform == 'ios': + config_option.append({ + 'label': 'Include Poster Image', + 'value': self.config['notification_type'], + 'name': 'remoteapp_notification_type', + 'description': 'Include a poster with the notifications.', + 'input_type': 'checkbox' + }) return config_option diff --git a/plexpy/version.py b/plexpy/version.py index 119e0b07..116f4687 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -18,4 +18,4 @@ from __future__ import unicode_literals PLEXPY_BRANCH = "master" -PLEXPY_RELEASE_VERSION = "v2.12.4" \ No newline at end of file +PLEXPY_RELEASE_VERSION = "v2.12.5" \ No newline at end of file diff --git a/plexpy/webauth.py b/plexpy/webauth.py index d105a8c2..5487f2ea 100644 --- a/plexpy/webauth.py +++ b/plexpy/webauth.py @@ -314,7 +314,7 @@ class AuthController(object): def get_loginform(self, redirect_uri=''): from plexpy.webserve import serve_template - return serve_template(templatename="login.html", title="Login", redirect_uri=unquote(redirect_uri)) + return serve_template(template_name="login.html", title="Login", redirect_uri=unquote(redirect_uri)) @cherrypy.expose def index(self, *args, **kwargs): diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 88b65174..b643f84b 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -51,6 +51,7 @@ if sys.version_info >= (3, 6): import plexpy if plexpy.PYTHON2: import activity_pinger + import activity_processor import common import config import database @@ -85,6 +86,7 @@ if plexpy.PYTHON2: import macos else: from plexpy import activity_pinger + from plexpy import activity_processor from plexpy import common from plexpy import config from plexpy import database @@ -119,12 +121,16 @@ else: from plexpy import macos -def serve_template(templatename, **kwargs): - interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/') - template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.INTERFACE) +TEMPLATE_LOOKUP = None - _hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'], - error_handler=mako_error_handler) + +def serve_template(template_name, **kwargs): + global TEMPLATE_LOOKUP + if TEMPLATE_LOOKUP is None: + interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/') + template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.INTERFACE) + TEMPLATE_LOOKUP = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'], + error_handler=mako_error_handler) http_root = plexpy.HTTP_ROOT server_name = helpers.pms_name() @@ -133,7 +139,7 @@ def serve_template(templatename, **kwargs): _session = get_session_info() try: - template = _hplookup.get_template(templatename) + template = TEMPLATE_LOOKUP.get_template(template_name) return template.render(http_root=http_root, server_name=server_name, cache_param=cache_param, _session=_session, **kwargs) except Exception as e: @@ -222,7 +228,7 @@ class WebInterface(object): plexpy.initialize_scheduler() raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT + "home") else: - return serve_template(templatename="welcome.html", title="Welcome", config=config) + return serve_template(template_name="welcome.html", title="Welcome", config=config) @cherrypy.expose @cherrypy.tools.json_out() @@ -287,7 +293,7 @@ class WebInterface(object): "update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG, "first_run_complete": plexpy.CONFIG.FIRST_RUN_COMPLETE } - return serve_template(templatename="index.html", title="Home", config=config) + return serve_template(template_name="index.html", title="Home", config=config) @cherrypy.expose @cherrypy.tools.json_out() @@ -332,10 +338,10 @@ class WebInterface(object): result = pms_connect.get_current_activity() if result: - return serve_template(templatename="current_activity.html", data=result) + return serve_template(template_name="current_activity.html", data=result) else: logger.warn("Unable to retrieve data for get_current_activity.") - return serve_template(templatename="current_activity.html", data=None) + return serve_template(template_name="current_activity.html", data=None) @cherrypy.expose @requireAuth() @@ -346,9 +352,9 @@ class WebInterface(object): if result: session = next((s for s in result['sessions'] if s['session_key'] == session_key), None) - return serve_template(templatename="current_activity_instance.html", session=session) + return serve_template(template_name="current_activity_instance.html", session=session) else: - return serve_template(templatename="current_activity_instance.html", session=None) + return serve_template(template_name="current_activity_instance.html", session=None) @cherrypy.expose @cherrypy.tools.json_out() @@ -391,7 +397,7 @@ class WebInterface(object): endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER) url = base_url + endpoint + ('?' + urlencode(kwargs) if kwargs else '') - return serve_template(templatename="xml_shortcut.html", title="Plex XML", url=url) + return serve_template(template_name="xml_shortcut.html", title="Plex XML", url=url) @cherrypy.expose @requireAuth() @@ -401,7 +407,7 @@ class WebInterface(object): stats_type=stats_type, stats_count=stats_count) - return serve_template(templatename="home_stats.html", title="Stats", data=stats_data) + return serve_template(template_name="home_stats.html", title="Stats", data=stats_data) @cherrypy.expose @requireAuth() @@ -412,7 +418,7 @@ class WebInterface(object): stats_data = data_factory.get_library_stats(library_cards=library_cards) - return serve_template(templatename="library_stats.html", title="Library Stats", data=stats_data) + return serve_template(template_name="library_stats.html", title="Library Stats", data=stats_data) @cherrypy.expose @requireAuth() @@ -422,13 +428,25 @@ class WebInterface(object): pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_recently_added_details(count=count, media_type=media_type) except IOError as e: - return serve_template(templatename="recently_added.html", data=None) + return serve_template(template_name="recently_added.html", data=None) if result and 'recently_added' in result: - return serve_template(templatename="recently_added.html", data=result['recently_added']) + return serve_template(template_name="recently_added.html", data=result['recently_added']) else: logger.warn("Unable to retrieve data for get_recently_added.") - return serve_template(templatename="recently_added.html", data=None) + return serve_template(template_name="recently_added.html", data=None) + + @cherrypy.expose + @cherrypy.tools.json_out() + @requireAuth(member_of("admin")) + @addtoapi() + def regroup_history(self, **kwargs): + """ Regroup play history in the database.""" + + threading.Thread(target=activity_processor.regroup_history).start() + + return {'result': 'success', + 'message': 'Regrouping play history started. Check the logs to monitor any problems.'} @cherrypy.expose @cherrypy.tools.json_out() @@ -463,7 +481,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() def libraries(self, **kwargs): - return serve_template(templatename="libraries.html", title="Libraries") + return serve_template(template_name="libraries.html", title="Libraries") @cherrypy.expose @cherrypy.tools.json_out() @@ -616,12 +634,12 @@ class WebInterface(object): library_details = library_data.get_details(section_id=section_id) except: logger.warn("Unable to retrieve library details for section_id %s " % section_id) - return serve_template(templatename="library.html", title="Library", data=None, config=config) + return serve_template(template_name="library.html", title="Library", data=None, config=config) else: logger.debug("Library page requested but no section_id received.") - return serve_template(templatename="library.html", title="Library", data=None, config=config) + return serve_template(template_name="library.html", title="Library", data=None, config=config) - return serve_template(templatename="library.html", title="Library", data=library_details, config=config) + return serve_template(template_name="library.html", title="Library", data=library_details, config=config) @cherrypy.expose @requireAuth(member_of("admin")) @@ -634,7 +652,7 @@ class WebInterface(object): result = None status_message = 'An error occured.' - return serve_template(templatename="edit_library.html", title="Edit Library", + return serve_template(template_name="edit_library.html", title="Edit Library", data=result, server_id=plexpy.CONFIG.PMS_IDENTIFIER, status_message=status_message) @cherrypy.expose @@ -681,7 +699,7 @@ class WebInterface(object): @requireAuth() def library_watch_time_stats(self, section_id=None, **kwargs): if not allow_session_library(section_id): - return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats") if section_id: library_data = libraries.Libraries() @@ -690,16 +708,16 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=result, title="Watch Stats") else: logger.warn("Unable to retrieve data for library_watch_time_stats.") - return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats") @cherrypy.expose @requireAuth() def library_user_stats(self, section_id=None, **kwargs): if not allow_session_library(section_id): - return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats") + return serve_template(template_name="library_user_stats.html", data=None, title="Player Stats") if section_id: library_data = libraries.Libraries() @@ -708,16 +726,16 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats") + return serve_template(template_name="library_user_stats.html", data=result, title="Player Stats") else: logger.warn("Unable to retrieve data for library_user_stats.") - return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats") + return serve_template(template_name="library_user_stats.html", data=None, title="Player Stats") @cherrypy.expose @requireAuth() def library_recently_watched(self, section_id=None, limit='10', **kwargs): if not allow_session_library(section_id): - return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched") + return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched") if section_id: library_data = libraries.Libraries() @@ -726,16 +744,16 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="user_recently_watched.html", data=result, title="Recently Watched") + return serve_template(template_name="user_recently_watched.html", data=result, title="Recently Watched") else: logger.warn("Unable to retrieve data for library_recently_watched.") - return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched") + return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched") @cherrypy.expose @requireAuth() def library_recently_added(self, section_id=None, limit='10', **kwargs): if not allow_session_library(section_id): - return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added") + return serve_template(template_name="library_recently_added.html", data=None, title="Recently Added") if section_id: pms_connect = pmsconnect.PmsConnect() @@ -744,10 +762,10 @@ class WebInterface(object): result = None if result and result['recently_added']: - return serve_template(templatename="library_recently_added.html", data=result['recently_added'], title="Recently Added") + return serve_template(template_name="library_recently_added.html", data=result['recently_added'], title="Recently Added") else: logger.warn("Unable to retrieve data for library_recently_added.") - return serve_template(templatename="library_recently_added.html", data=None, title="Recently Added") + return serve_template(template_name="library_recently_added.html", data=None, title="Recently Added") @cherrypy.expose @cherrypy.tools.json_out() @@ -1239,7 +1257,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() def users(self, **kwargs): - return serve_template(templatename="users.html", title="Users") + return serve_template(template_name="users.html", title="Users") @cherrypy.expose @cherrypy.tools.json_out() @@ -1355,12 +1373,12 @@ class WebInterface(object): user_details = user_data.get_details(user_id=user_id) except: logger.warn("Unable to retrieve user details for user_id %s " % user_id) - return serve_template(templatename="user.html", title="User", data=None) + return serve_template(template_name="user.html", title="User", data=None) else: logger.debug("User page requested but no user_id received.") - return serve_template(templatename="user.html", title="User", data=None) + return serve_template(template_name="user.html", title="User", data=None) - return serve_template(templatename="user.html", title="User", data=user_details) + return serve_template(template_name="user.html", title="User", data=user_details) @cherrypy.expose @requireAuth(member_of("admin")) @@ -1373,7 +1391,7 @@ class WebInterface(object): result = None status_message = 'An error occured.' - return serve_template(templatename="edit_user.html", title="Edit User", data=result, status_message=status_message) + return serve_template(template_name="edit_user.html", title="Edit User", data=result, status_message=status_message) @cherrypy.expose @requireAuth(member_of("admin")) @@ -1421,7 +1439,7 @@ class WebInterface(object): @requireAuth() def user_watch_time_stats(self, user=None, user_id=None, **kwargs): if not allow_session_user(user_id): - return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats") if user_id or user: user_data = users.Users() @@ -1430,16 +1448,16 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=result, title="Watch Stats") else: logger.warn("Unable to retrieve data for user_watch_time_stats.") - return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats") @cherrypy.expose @requireAuth() def user_player_stats(self, user=None, user_id=None, **kwargs): if not allow_session_user(user_id): - return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats") + return serve_template(template_name="user_player_stats.html", data=None, title="Player Stats") if user_id or user: user_data = users.Users() @@ -1448,16 +1466,16 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="user_player_stats.html", data=result, title="Player Stats") + return serve_template(template_name="user_player_stats.html", data=result, title="Player Stats") else: logger.warn("Unable to retrieve data for user_player_stats.") - return serve_template(templatename="user_player_stats.html", data=None, title="Player Stats") + return serve_template(template_name="user_player_stats.html", data=None, title="Player Stats") @cherrypy.expose @requireAuth() def get_user_recently_watched(self, user=None, user_id=None, limit='10', **kwargs): if not allow_session_user(user_id): - return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched") + return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched") if user_id or user: user_data = users.Users() @@ -1466,10 +1484,10 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="user_recently_watched.html", data=result, title="Recently Watched") + return serve_template(template_name="user_recently_watched.html", data=result, title="Recently Watched") else: logger.warn("Unable to retrieve data for get_user_recently_watched.") - return serve_template(templatename="user_recently_watched.html", data=None, title="Recently Watched") + return serve_template(template_name="user_recently_watched.html", data=None, title="Recently Watched") @cherrypy.expose @cherrypy.tools.json_out() @@ -1875,7 +1893,7 @@ class WebInterface(object): "database_is_importing": database.IS_IMPORTING, } - return serve_template(templatename="history.html", title="History", config=config) + return serve_template(template_name="history.html", title="History", config=config) @cherrypy.expose @cherrypy.tools.json_out() @@ -2063,7 +2081,7 @@ class WebInterface(object): data_factory = datafactory.DataFactory() stream_data = data_factory.get_stream_details(row_id, session_key) - return serve_template(templatename="stream_data.html", title="Stream Data", data=stream_data, user=user) + return serve_template(template_name="stream_data.html", title="Stream Data", data=stream_data, user=user) @cherrypy.expose @cherrypy.tools.json_out() @@ -2154,7 +2172,7 @@ class WebInterface(object): public = helpers.is_public_ip(ip_address) - return serve_template(templatename="ip_address_modal.html", title="IP Address Details", + return serve_template(template_name="ip_address_modal.html", title="IP Address Details", data=ip_address, public=public, kwargs=kwargs) @cherrypy.expose @@ -2193,7 +2211,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() def graphs(self, **kwargs): - return serve_template(templatename="graphs.html", title="Graphs") + return serve_template(template_name="graphs.html", title="Graphs") @cherrypy.expose @cherrypy.tools.json_out() @@ -2238,7 +2256,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2282,7 +2300,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2326,7 +2344,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2370,7 +2388,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of months of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2414,7 +2432,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2458,7 +2476,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2502,7 +2520,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2545,7 +2563,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2588,7 +2606,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2631,7 +2649,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2674,7 +2692,7 @@ class WebInterface(object): Optional parameters: time_range (str): The number of days of data to return y_axis (str): "plays" or "duration" - user_id (str): The user id to filter the data + user_id (str): Comma separated list of user id to filter the data grouping (int): 0 or 1 Returns: @@ -2707,9 +2725,9 @@ class WebInterface(object): @requireAuth() def history_table_modal(self, **kwargs): if kwargs.get('user_id') and not allow_session_user(kwargs['user_id']): - return serve_template(templatename="history_table_modal.html", title="History Data", data=None) + return serve_template(template_name="history_table_modal.html", title="History Data", data=None) - return serve_template(templatename="history_table_modal.html", title="History Data", data=kwargs) + return serve_template(template_name="history_table_modal.html", title="History Data", data=kwargs) ##### Sync ##### @@ -2717,7 +2735,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() def sync(self, **kwargs): - return serve_template(templatename="sync.html", title="Synced Items") + return serve_template(template_name="sync.html", title="Synced Items") @cherrypy.expose @cherrypy.tools.json_out() @@ -2776,7 +2794,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def logs(self, **kwargs): plex_log_files = log_reader.list_plex_logs() - return serve_template(templatename="logs.html", title="Log", plex_log_files=plex_log_files) + return serve_template(template_name="logs.html", title="Log", plex_log_files=plex_log_files) @cherrypy.expose @requireAuth(member_of("admin")) @@ -3170,7 +3188,7 @@ class WebInterface(object): for key in ('home_sections', 'home_stats_cards', 'home_library_cards'): settings_dict[key] = json.dumps(settings_dict[key]) - return serve_template(templatename="settings.html", title="Settings", config=settings_dict) + return serve_template(template_name="settings.html", title="Settings", config=settings_dict) @cherrypy.expose @cherrypy.tools.json_out() @@ -3361,17 +3379,17 @@ class WebInterface(object): @cherrypy.expose @requireAuth(member_of("admin")) def get_configuration_table(self, **kwargs): - return serve_template(templatename="configuration_table.html") + return serve_template(template_name="configuration_table.html") @cherrypy.expose @requireAuth(member_of("admin")) def get_scheduler_table(self, **kwargs): - return serve_template(templatename="scheduler_table.html") + return serve_template(template_name="scheduler_table.html") @cherrypy.expose @requireAuth(member_of("admin")) def get_queue_modal(self, queue=None, **kwargs): - return serve_template(templatename="queue_modal.html", queue=queue) + return serve_template(template_name="queue_modal.html", queue=queue) @cherrypy.expose @cherrypy.tools.json_out() @@ -3435,7 +3453,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def get_notifiers_table(self, **kwargs): result = notifiers.get_notifiers() - return serve_template(templatename="notifiers_table.html", notifiers_list=result) + return serve_template(template_name="notifiers_table.html", notifiers_list=result) @cherrypy.expose @cherrypy.tools.json_out() @@ -3518,7 +3536,7 @@ class WebInterface(object): for category in common.NOTIFICATION_PARAMETERS for param in category['parameters'] ] - return serve_template(templatename="notifier_config.html", notifier=result, parameters=parameters) + return serve_template(template_name="notifier_config.html", notifier=result, parameters=parameters) @cherrypy.expose @cherrypy.tools.json_out() @@ -3601,7 +3619,7 @@ class WebInterface(object): text.append({'media_type': media_type, 'subject': test_subject, 'body': test_body}) - return serve_template(templatename="notifier_text_preview.html", text=text, agent=agent_name) + return serve_template(template_name="notifier_text_preview.html", text=text, agent=agent_name) @cherrypy.expose @cherrypy.tools.json_out() @@ -3779,7 +3797,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def get_mobile_devices_table(self, **kwargs): result = mobile_app.get_mobile_devices() - return serve_template(templatename="mobile_devices_table.html", devices_list=result) + return serve_template(template_name="mobile_devices_table.html", devices_list=result) @cherrypy.expose @cherrypy.tools.json_out() @@ -3802,7 +3820,7 @@ class WebInterface(object): def get_mobile_device_config_modal(self, mobile_device_id=None, **kwargs): result = mobile_app.get_mobile_device_config(mobile_device_id=mobile_device_id) - return serve_template(templatename="mobile_device_config.html", device=result) + return serve_template(template_name="mobile_device_config.html", device=result) @cherrypy.expose @cherrypy.tools.json_out() @@ -4012,11 +4030,11 @@ class WebInterface(object): @requireAuth(member_of("admin")) def import_database_tool(self, app=None, **kwargs): if app == 'tautulli': - return serve_template(templatename="app_import.html", title="Import Tautulli Database", app="Tautulli") + return serve_template(template_name="app_import.html", title="Import Tautulli Database", app="Tautulli") elif app == 'plexwatch': - return serve_template(templatename="app_import.html", title="Import PlexWatch Database", app="PlexWatch") + return serve_template(template_name="app_import.html", title="Import PlexWatch Database", app="PlexWatch") elif app == 'plexivity': - return serve_template(templatename="app_import.html", title="Import Plexivity Database", app="Plexivity") + return serve_template(template_name="app_import.html", title="Import Plexivity Database", app="Plexivity") logger.warn("No app specified for import.") return @@ -4024,7 +4042,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth(member_of("admin")) def import_config_tool(self, **kwargs): - return serve_template(templatename="config_import.html", title="Import Tautulli Configuration") + return serve_template(template_name="config_import.html", title="Import Tautulli Configuration") @cherrypy.expose @cherrypy.tools.json_out() @@ -4284,8 +4302,6 @@ class WebInterface(object): return update - @cherrypy.expose - @requireAuth(member_of("admin")) def do_state_change(self, signal, title, timer, **kwargs): message = title quote = self.random_arnold_quotes() @@ -4297,7 +4313,7 @@ class WebInterface(object): else: new_http_root = '/' - return serve_template(templatename="shutdown.html", signal=signal, title=title, + return serve_template(template_name="shutdown.html", signal=signal, title=title, new_http_root=new_http_root, message=message, timer=timer, quote=quote) @cherrypy.expose @@ -4407,7 +4423,7 @@ class WebInterface(object): if metadata['section_id'] and not allow_session_library(metadata['section_id']): raise cherrypy.HTTPRedirect(plexpy.HTTP_ROOT) - return serve_template(templatename="info.html", metadata=metadata, title="Info", + return serve_template(template_name="info.html", metadata=metadata, title="Info", config=config, source=source, user_info=user_info) else: if get_session_user_id(): @@ -4423,11 +4439,11 @@ class WebInterface(object): result = pms_connect.get_item_children(rating_key=rating_key, media_type=media_type) if result: - return serve_template(templatename="info_children_list.html", data=result, + return serve_template(template_name="info_children_list.html", data=result, media_type=media_type, title="Children List") else: logger.warn("Unable to retrieve data for get_item_children.") - return serve_template(templatename="info_children_list.html", data=None, title="Children List") + return serve_template(template_name="info_children_list.html", data=None, title="Children List") @cherrypy.expose @requireAuth() @@ -4437,9 +4453,9 @@ class WebInterface(object): result = pms_connect.get_item_children_related(rating_key=rating_key) if result: - return serve_template(templatename="info_collection_list.html", data=result, title=title) + return serve_template(template_name="info_collection_list.html", data=result, title=title) else: - return serve_template(templatename="info_collection_list.html", data=None, title=title) + return serve_template(template_name="info_collection_list.html", data=None, title=title) @cherrypy.expose @requireAuth() @@ -4451,10 +4467,10 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=result, title="Watch Stats") else: logger.warn("Unable to retrieve data for item_watch_time_stats.") - return serve_template(templatename="user_watch_time_stats.html", data=None, title="Watch Stats") + return serve_template(template_name="user_watch_time_stats.html", data=None, title="Watch Stats") @cherrypy.expose @requireAuth() @@ -4466,10 +4482,10 @@ class WebInterface(object): result = None if result: - return serve_template(templatename="library_user_stats.html", data=result, title="Player Stats") + return serve_template(template_name="library_user_stats.html", data=result, title="Player Stats") else: logger.warn("Unable to retrieve data for item_user_stats.") - return serve_template(templatename="library_user_stats.html", data=None, title="Player Stats") + return serve_template(template_name="library_user_stats.html", data=None, title="Player Stats") @cherrypy.expose @cherrypy.tools.json_out() @@ -5091,7 +5107,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth() def search(self, query='', **kwargs): - return serve_template(templatename="search.html", title="Search", query=query) + return serve_template(template_name="search.html", title="Search", query=query) @cherrypy.expose @cherrypy.tools.json_out() @@ -5148,10 +5164,10 @@ class WebInterface(object): if season['media_index'] == season_index] if result: - return serve_template(templatename="info_search_results_list.html", data=result, title="Search Result List") + return serve_template(template_name="info_search_results_list.html", data=result, title="Search Result List") else: logger.warn("Unable to retrieve data for get_search_results_children.") - return serve_template(templatename="info_search_results_list.html", data=None, title="Search Result List") + return serve_template(template_name="info_search_results_list.html", data=None, title="Search Result List") ##### Update Metadata ##### @@ -5168,10 +5184,10 @@ class WebInterface(object): query['query_string'] = query_string if query: - return serve_template(templatename="update_metadata.html", query=query, update=update, title="Info") + return serve_template(template_name="update_metadata.html", query=query, update=update, title="Info") else: logger.warn("Unable to retrieve data for update_metadata.") - return serve_template(templatename="update_metadata.html", query=query, update=update, title="Info") + return serve_template(template_name="update_metadata.html", query=query, update=update, title="Info") @cherrypy.expose @cherrypy.tools.json_out() @@ -6574,7 +6590,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def get_newsletters_table(self, **kwargs): result = newsletters.get_newsletters() - return serve_template(templatename="newsletters_table.html", newsletters_list=result) + return serve_template(template_name="newsletters_table.html", newsletters_list=result) @cherrypy.expose @cherrypy.tools.json_out() @@ -6649,7 +6665,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def get_newsletter_config_modal(self, newsletter_id=None, **kwargs): result = newsletters.get_newsletter_config(newsletter_id=newsletter_id, mask_passwords=True) - return serve_template(templatename="newsletter_config.html", newsletter=result) + return serve_template(template_name="newsletter_config.html", newsletter=result) @cherrypy.expose @cherrypy.tools.json_out() @@ -6757,7 +6773,7 @@ class WebInterface(object): elif kwargs.pop('key', None) == plexpy.CONFIG.NEWSLETTER_PASSWORD: return self.newsletter_auth(*args, **kwargs) else: - return serve_template(templatename="newsletter_auth.html", + return serve_template(template_name="newsletter_auth.html", title="Newsletter Login", uri=request_uri) @@ -6794,7 +6810,7 @@ class WebInterface(object): @requireAuth(member_of("admin")) def newsletter_preview(self, **kwargs): kwargs['preview'] = 'true' - return serve_template(templatename="newsletter_preview.html", + return serve_template(template_name="newsletter_preview.html", title="Newsletter", kwargs=kwargs) @@ -6833,7 +6849,7 @@ class WebInterface(object): @cherrypy.expose @requireAuth(member_of("admin")) def support(self, **kwargs): - return serve_template(templatename="support.html", title="Support") + return serve_template(template_name="support.html", title="Support") @cherrypy.expose @cherrypy.tools.json_out() @@ -6988,7 +7004,7 @@ class WebInterface(object): if media_type == 'photo_album': media_type = 'photoalbum' - return serve_template(templatename="export_modal.html", title="Export Metadata", + return serve_template(template_name="export_modal.html", title="Export Metadata", section_id=section_id, user_id=user_id, rating_key=rating_key, media_type=media_type, sub_media_type=sub_media_type, export_type=export_type, file_formats=file_formats)