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()"> + %def> @@ -14,9 +15,7 @@
');y[2]&&($=$.replace("{var}",y[2][1Change 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)