Compare commits
38 Commits
v2.1.10-be
...
v2.1.13
Author | SHA1 | Date | |
---|---|---|---|
![]() |
70b6bd4efd | ||
![]() |
4f4a99d695 | ||
![]() |
f65baa22f1 | ||
![]() |
a1fb081d47 | ||
![]() |
63ac3eec98 | ||
![]() |
73c11c053d | ||
![]() |
85c398ce05 | ||
![]() |
7387cb8322 | ||
![]() |
37841b6e8c | ||
![]() |
af1355e220 | ||
![]() |
37bd005907 | ||
![]() |
311030b58e | ||
![]() |
bdd124327f | ||
![]() |
70e5e698fa | ||
![]() |
33e799955e | ||
![]() |
331be52327 | ||
![]() |
6ece690e23 | ||
![]() |
6b63a1399e | ||
![]() |
5c77cf652b | ||
![]() |
d6f9a82edb | ||
![]() |
a887489666 | ||
![]() |
8c0bcd0059 | ||
![]() |
c18ee81130 | ||
![]() |
44428cc6e5 | ||
![]() |
c19cc858bd | ||
![]() |
668913fd60 | ||
![]() |
50c5407a46 | ||
![]() |
939755d3b7 | ||
![]() |
54f4696713 | ||
![]() |
c85af521fe | ||
![]() |
917d19db85 | ||
![]() |
7292f25eb9 | ||
![]() |
22a2ad4bc7 | ||
![]() |
95e56f5ea5 | ||
![]() |
ed24232a0a | ||
![]() |
15225faee7 | ||
![]() |
041a35a35a | ||
![]() |
6d365c174a |
8
API.md
8
API.md
@@ -2624,15 +2624,15 @@ Returns:
|
||||
|
||||
|
||||
### terminate_session
|
||||
Add a new notification agent.
|
||||
Stop a streaming session.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
session_id (str): The id of the session to terminate
|
||||
message (str): A custom message to send to the client
|
||||
session_key (int): The session key of the session to terminate, OR
|
||||
session_id (str): The session id of the session to terminate
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
message (str): A custom message to send to the client
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
32
CHANGELOG.md
32
CHANGELOG.md
@@ -1,5 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## v2.1.13 (2018-06-16)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Soft crash when viewing photos not in an album.
|
||||
* Notifications:
|
||||
* New: Added current date and time notification parameters.
|
||||
* UI:
|
||||
* New: Added support page with embedded Discord chat using WidgetBot.
|
||||
|
||||
|
||||
## v2.1.12 (2018-06-08)
|
||||
|
||||
* Notifications:
|
||||
* Change: Blank notification link source means disabled instead of default.
|
||||
* Newsletters:
|
||||
* New: Make collection tags available in the raw newsletter data for custom templates.
|
||||
* API:
|
||||
* New: Ability to terminate a stream using the session key.
|
||||
|
||||
|
||||
## v2.1.11-beta (2018-06-02)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Activity progress bar not updating in some cases.
|
||||
* Fix: Monitory Remote Access setting disabled due to Plex Media Server API changes.
|
||||
* Change: Improved logic for grouping history items without being successive plays.
|
||||
* Notifications:
|
||||
* New: Added filename to notification parameters.
|
||||
* Other:
|
||||
* Fix: Update metadata failing for tracks without track numbers.
|
||||
|
||||
|
||||
## v2.1.10-beta (2018-05-28)
|
||||
|
||||
* Monitoring:
|
||||
|
@@ -1,6 +1,6 @@
|
||||
# Tautulli
|
||||
|
||||
[](https://discord.gg/tQcWEUp)
|
||||
[](https://tautulli.com/discord)
|
||||
[](https://www.reddit.com/r/Tautulli/)
|
||||
[](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server)
|
||||
|
||||
@@ -35,7 +35,7 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
|
||||
|
||||
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
|
||||
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
|
||||
* Support is available on [Discord](https://discord.gg/tQcWEUp), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server).
|
||||
* Support is available on [Discord](https://tautulli.com/discord), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server).
|
||||
|
||||
## Issues & Feature Requests
|
||||
|
||||
|
@@ -17,7 +17,6 @@
|
||||
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
|
||||
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
|
||||
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
|
||||
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
||||
${next.headIncludes()}
|
||||
|
||||
<!-- Favicons -->
|
||||
@@ -134,7 +133,7 @@
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
|
||||
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
|
||||
<li><a href="settings?support=true"><i class="fa fa-fw fa-comment"></i> Support</a></li>
|
||||
<li><a href="support"><i class="fa fa-fw fa-comment"></i> Support</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
|
||||
<li role="separator" class="divider"></li>
|
||||
@@ -289,9 +288,13 @@ ${next.modalIncludes()}
|
||||
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
|
||||
<script src="${http_root}js/bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
|
||||
<script>window.FontAwesomeConfig = {searchPseudoElements: true}</script>
|
||||
<script src="${http_root}js/fontawesome-v5.0.13.min.js"></script>
|
||||
<script src="${http_root}js/fontawesome-v4-shims.min.js"></script>
|
||||
<script src="${http_root}js/pnotify.custom.min.js"></script>
|
||||
<script src="${http_root}js/script.js${cache_param}"></script>
|
||||
<script src="${http_root}js/jquery.qrcode.min.js"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
|
||||
<script src="${http_root}js/ajaxNotifications.js"></script>
|
||||
% endif
|
||||
|
@@ -88,7 +88,7 @@ DOCUMENTATION :: END
|
||||
<tr>
|
||||
<td>Support:</td>
|
||||
<td>
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/tQcWEUp')}" target="_blank">Tautulli Discord Server</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://tautulli.com/discord')}" target="_blank">Tautulli Discord Server</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
|
||||
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server')}" target="_blank">Plex Forums</a>
|
||||
</td>
|
||||
|
@@ -113,6 +113,10 @@ div.form-control .selectize-input {
|
||||
.wizard-input-section .selectize-dropdown.form-control.selectize-pms-ip {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
#condition-widget .fa-plus,
|
||||
#condition-widget .fa-minus {
|
||||
cursor: pointer;
|
||||
}
|
||||
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
|
||||
color: #fff !important;
|
||||
}
|
||||
@@ -1599,10 +1603,8 @@ a:hover .dashboard-recent-media-cover {
|
||||
margin: 0;
|
||||
background: none;
|
||||
}
|
||||
.summary-navbar-list .breadcrumb > li + li:before {
|
||||
.summary-navbar-list .breadcrumb > .breadcrumb-arrow {
|
||||
color: #444;
|
||||
font-family: FontAwesome;
|
||||
content: "\f054";
|
||||
padding: 0 15px;
|
||||
}
|
||||
.summary-navbar-list .breadcrumb > .active {
|
||||
@@ -2113,21 +2115,18 @@ a:hover .item-children-poster {
|
||||
}
|
||||
.settings-alert ul li {
|
||||
list-style: none;
|
||||
padding: 5px 12px 5px 35px;
|
||||
padding: 5px 12px;
|
||||
margin: 0;
|
||||
border: 1px solid #ebccd1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.settings-alert ul li:before {
|
||||
content: "\f071";
|
||||
font-family: FontAwesome;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 12px;
|
||||
display: none;
|
||||
content: "\f071 ";
|
||||
font-family: "Font Awesome 5 Solid";
|
||||
}
|
||||
.settings-alert ul li .svg-inline--fa {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.settings-warning {
|
||||
color: #eb8600;
|
||||
@@ -2957,10 +2956,13 @@ a .home-platforms-list-cover-face:hover
|
||||
background: #282828;
|
||||
list-style: none;
|
||||
}
|
||||
.accordion .link {
|
||||
.accordion li {
|
||||
margin: 0;
|
||||
}
|
||||
.accordion li .link {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
padding: 8px 20px 8px 30px;
|
||||
padding: 8px 12px 8px 12px;
|
||||
color: #999;
|
||||
border-bottom: 1px solid #2d2d2d;
|
||||
position: relative;
|
||||
@@ -2968,37 +2970,28 @@ a .home-platforms-list-cover-face:hover
|
||||
-o-transition: all 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.accordion li {
|
||||
margin: 0;
|
||||
.accordion li .link:hover {
|
||||
color: #fff;
|
||||
background: #2f2f2f;
|
||||
}
|
||||
.accordion li .link span.toggle-right {
|
||||
float: right;
|
||||
padding-left: 10px;
|
||||
}
|
||||
.accordion li:last-child .link {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.accordion li i.fa {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
.accordion li .fa-chevron-down {
|
||||
color: #999;
|
||||
-webkit-transition: all 0.3s ease;
|
||||
-o-transition: all 0.3s ease;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.accordion li i.fa-chevron-down {
|
||||
right: 12px;
|
||||
left: auto;
|
||||
font-size: 16px;
|
||||
}
|
||||
.accordion li .link:hover {
|
||||
color: #FFF;
|
||||
background: #2f2f2f;
|
||||
}
|
||||
.accordion li.open .link {
|
||||
color: #f9be03;
|
||||
}
|
||||
.accordion li.open i {
|
||||
.accordion li.open .fa-chevron-down {
|
||||
color: #f9be03;
|
||||
}
|
||||
.accordion li.open i.fa-chevron-down {
|
||||
-webkit-transform: rotate(180deg);
|
||||
-ms-transform: rotate(180deg);
|
||||
-o-transform: rotate(180deg);
|
||||
@@ -3156,34 +3149,6 @@ div.dataTables_info {
|
||||
-o-transition: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.settings-alert {
|
||||
float: left;
|
||||
padding: 0;
|
||||
margin: 5px 0;
|
||||
border: 0;
|
||||
position: relative;
|
||||
}
|
||||
.settings-alert ul {
|
||||
padding: 0;
|
||||
}
|
||||
.settings-alert ul li {
|
||||
list-style: none;
|
||||
padding: 5px 12px 5px 35px;
|
||||
margin: 0;
|
||||
border: 1px solid #ebccd1;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.settings-alert ul li:before {
|
||||
content: "\f071";
|
||||
font-family: FontAwesome;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
text-decoration: inherit;
|
||||
font-size: 18px;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 12px;
|
||||
}
|
||||
#users-to-delete > li,
|
||||
#users-to-purge > li,
|
||||
#libraries-to-delete > li,
|
||||
@@ -4096,3 +4061,62 @@ a[data-tab-destination] {
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.iframe-container {
|
||||
width: 100%;
|
||||
height: calc(100vh - 200px);
|
||||
position: relative;
|
||||
}
|
||||
.iframe-overlay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: url(../images/discord-overlay.png) no-repeat;
|
||||
background-size: cover;
|
||||
border: 1px solid #36393e;
|
||||
}
|
||||
.iframe-button-container {
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
.iframe-button {
|
||||
color: #fff;
|
||||
border-radius: 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
min-width: 200px;
|
||||
transition: box-shadow 0.3s ease;
|
||||
padding: 0 15px;
|
||||
background: rgba(114, 137, 218, 0.4);
|
||||
text-transform: uppercase;
|
||||
text-decoration: none;
|
||||
display: block;
|
||||
}
|
||||
.iframe-button:hover,
|
||||
.iframe-button:focus {
|
||||
color: #fff;
|
||||
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 99999px inset, rgba(0, 0, 0, 0.2) 0px 1px 5px 0px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 3px 1px -2px;
|
||||
}
|
||||
.iframe-button:active {
|
||||
box-shadow: rgba(255, 255, 255, 0.1) 0px 0px 0px 99999px inset, rgba(0, 0, 0, 0.2) 0px 5px 5px -3px, rgba(0, 0, 0, 0.14) 0px 8px 10px 1px, rgba(0, 0, 0, 0.12) 0px 3px 14px 2px;
|
||||
}
|
||||
.iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: inherit;
|
||||
display: block;
|
||||
border: 0;
|
||||
}
|
||||
.fa-blank {
|
||||
visibility: hidden;
|
||||
}
|
@@ -168,7 +168,7 @@
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||
<h4><i class="fa fa-expand-arrows-alt"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||
<p class="help-block">
|
||||
The combined total of tv and movies by their original resolution (pre-transcoding).
|
||||
</p>
|
||||
@@ -181,7 +181,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||
<h4><i class="fa fa-expand-arrows-alt"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4>
|
||||
<p class="help-block">
|
||||
The combined total of tv and movies by their streamed resolution (post-transcoding).
|
||||
</p>
|
||||
|
BIN
data/interfaces/default/images/discord-overlay.png
Normal file
BIN
data/interfaces/default/images/discord-overlay.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 260 KiB |
@@ -12,7 +12,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="padded-header" id="current-activity-header">
|
||||
<h3><span id="sessions-shortcut">Activity</span>
|
||||
<h3><span id="sessions-xml">Activity</span>
|
||||
<small>
|
||||
<span id="currentActivityHeader" style="display: none;">
|
||||
Streams: <span id="currentActivityHeader-streams"></span> |
|
||||
@@ -236,7 +236,6 @@
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
|
||||
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
<script>
|
||||
var date_format = 'YYYY-MM-DD';
|
||||
var time_format = 'hh:mm a';
|
||||
@@ -546,7 +545,7 @@
|
||||
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
|
||||
var progress_bar = $('#progress-bar-' + key);
|
||||
progress_bar.data('state', s.state);
|
||||
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
|
||||
if (progress_bar.data('last_view_offset') !== s.view_offset) {
|
||||
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
|
||||
}
|
||||
|
||||
@@ -676,6 +675,7 @@
|
||||
$.ajax({
|
||||
url: 'terminate_session',
|
||||
data: {
|
||||
session_key: key,
|
||||
session_id: session_id,
|
||||
message: message
|
||||
},
|
||||
@@ -694,10 +694,8 @@
|
||||
});
|
||||
});
|
||||
|
||||
$('#sessions-shortcut').on('tripleclick', function () {
|
||||
$.getJSON('return_sessions_url', function(sessions_url) {
|
||||
window.open(sessions_url, '_blank');
|
||||
});
|
||||
$('#sessions-xml').on('tripleclick', function () {
|
||||
openPlexXML('/status/sessions');
|
||||
});
|
||||
% endif
|
||||
</script>
|
||||
|
@@ -83,31 +83,44 @@ DOCUMENTATION :: END
|
||||
<ul class="list-unstyled breadcrumb">
|
||||
% if data['media_type'] in ('movie', 'collection'):
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<li class="active">${data['title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'show':
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<li class="active">${data['title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'season':
|
||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||
<li class="active">Season ${data['media_index']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Season ${data['media_index']}</li>
|
||||
% elif data['media_type'] == 'episode':
|
||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
|
||||
<li class="active">Episode ${data['media_index']} - ${data['title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
|
||||
% elif data['media_type'] == 'artist':
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<li class="active">${data['title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'album':
|
||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||
<li class="active">${data['title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">${data['title']}</li>
|
||||
% elif data['media_type'] == 'track':
|
||||
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
|
||||
<li class="active">Track ${data['media_index']} - ${data['title']}</li>
|
||||
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
|
||||
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
@@ -703,6 +716,10 @@ DOCUMENTATION :: END
|
||||
</script>
|
||||
% endif
|
||||
<script>
|
||||
$('.metadata-xml').on('tripleclick', function () {
|
||||
openPlexXML("/library/metadata/${data['rating_key']}");
|
||||
});
|
||||
|
||||
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
|
||||
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
|
File diff suppressed because one or more lines are too long
5
data/interfaces/default/js/fontawesome-v4-shims.min.js
vendored
Normal file
5
data/interfaces/default/js/fontawesome-v4-shims.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
data/interfaces/default/js/fontawesome-v5.0.13.min.js
vendored
Normal file
5
data/interfaces/default/js/fontawesome-v5.0.13.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -37,7 +37,7 @@ function showMsg(msg, loader, timeout, ms, error) {
|
||||
}
|
||||
var message = $("<div class='msg'>" + msg + "</div>");
|
||||
if (loader) {
|
||||
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
message = $("<div class='msg'><i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
||||
feedback.css("padding", "14px 10px");
|
||||
}
|
||||
if (error) {
|
||||
@@ -103,14 +103,14 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
dataString = $(formID).serialize();
|
||||
}
|
||||
// Loader Image
|
||||
var loader = $("<i class='fa fa-refresh fa-spin'></i>");
|
||||
var loader = $("<i class='fa fa-refresh fa-spin ajaxLoader-" + url +"></i>");
|
||||
// Data Success Message
|
||||
var dataSucces = $(elem).data('success');
|
||||
if (typeof dataSucces === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
dataSucces = "Success!";
|
||||
}
|
||||
// Data Errror Message
|
||||
// Data Error Message
|
||||
var dataError = $(elem).data('error');
|
||||
if (typeof dataError === "undefined") {
|
||||
// Standard Message when variable is not set
|
||||
@@ -187,7 +187,7 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
|
||||
},
|
||||
complete: function (jqXHR, textStatus) {
|
||||
// Remove loaders and stuff, ajax request is complete!
|
||||
loader.remove();
|
||||
feedback.remove('.ajaxLoader-' + url);
|
||||
if (typeof callback === "function") {
|
||||
callback(jqXHR);
|
||||
}
|
||||
@@ -457,4 +457,11 @@ function capitalizeFirstLetter(string) {
|
||||
|
||||
$.fn.slideToggleBool = function(bool, options) {
|
||||
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
|
||||
};
|
||||
};
|
||||
|
||||
function openPlexXML(endpoint, plextv, params) {
|
||||
var data = $.extend({endpoint: endpoint, plextv: plextv}, params);
|
||||
$.getJSON('return_plex_xml_url', data, function(xml_url) {
|
||||
window.open(xml_url, '_blank');
|
||||
});
|
||||
}
|
||||
|
@@ -67,7 +67,7 @@ history_table_options = {
|
||||
expand_history = '<span class="expand-history-tooltip" data-toggle="tooltip" title="Show Detailed History"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
||||
$(td).html('<div><a href="#"><div style="float: left;">' + expand_history + ' ' + date + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i> ' + date + '</div>');
|
||||
$(td).html('<div style="float: left;"><i class="fa fa-plus-circle fa-fw fa-blank"> </i> ' + date + '</div>');
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
|
@@ -58,7 +58,7 @@ media_info_table_options = {
|
||||
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
|
||||
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + ' ' + date + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html('<div style="float: left;"><i class="fa fa-fw"></i> ' + date + '</div>');
|
||||
$(td).html('<div style="float: left;"><i class="fa fa-plus-circle fa-fw fa-blank"></i> ' + date + '</div>');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<div class='container-fluid'>
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-book"></i> All Libraries</span>
|
||||
<span id="libraries-xml"><i class="fa fa-book"></i> All Libraries</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -198,5 +198,9 @@
|
||||
});
|
||||
});
|
||||
% endif
|
||||
|
||||
$('#libraries-xml').on('tripleclick', function () {
|
||||
openPlexXML('/library/sections/all');
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
@@ -202,7 +202,11 @@
|
||||
% if notifier['agent_name'] == 'scripts':
|
||||
% for action in available_notification_actions:
|
||||
<li>
|
||||
<div class="link"><i class="fa ${action['icon']} fa-fw"></i> ${action['label']}<i class="fa fa-chevron-down"></i></div>
|
||||
<div class="link">
|
||||
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>
|
||||
${action['label']}
|
||||
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<ul class="submenu">
|
||||
<li>
|
||||
<div class="form-group">
|
||||
@@ -224,7 +228,11 @@
|
||||
% else:
|
||||
% for action in available_notification_actions:
|
||||
<li>
|
||||
<div class="link"><i class="fa ${action['icon']} fa-fw"></i> ${action['label']}<i class="fa fa-chevron-down"></i></div>
|
||||
<div class="link">
|
||||
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>
|
||||
${action['label']}
|
||||
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
|
||||
</div>
|
||||
<ul class="submenu">
|
||||
<li>
|
||||
<div class="form-group">
|
||||
|
@@ -115,9 +115,9 @@
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Successive Play History
|
||||
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Play History
|
||||
</label>
|
||||
<p class="help-block">Group successive play history by the same user as a single entry in the watch statistics, tables, and graphs.</p>
|
||||
<p class="help-block">Group play history for the same item and user as a single entry when progress is less than the watched percent.</p>
|
||||
</div>
|
||||
<div class="checkbox advanced-setting">
|
||||
<label>
|
||||
@@ -646,7 +646,7 @@
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-plex_media_server">
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
|
||||
<h3 id="resources-xml">Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
|
||||
</div>
|
||||
|
||||
<div class="form-group has-feedback" id="pms_ip_group">
|
||||
@@ -1763,9 +1763,6 @@
|
||||
} else if ("${kwargs.get('reinstall_geoip')}" == 'true') {
|
||||
$('#reinstall_geoip_db').removeClass('no-highlight').css('color','#e9a049');
|
||||
}
|
||||
if ("${kwargs.get('support')}" == 'true') {
|
||||
$('.support-modal-link').removeClass('no-highlight').css('color','#e9a049');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -2358,7 +2355,7 @@ $(document).ready(function() {
|
||||
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||
async: true,
|
||||
success: function(data) {
|
||||
if (data !== 'true') {
|
||||
if (data === 'false' || data === '0') {
|
||||
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
|
||||
}
|
||||
@@ -2753,6 +2750,10 @@ $(document).ready(function() {
|
||||
body_container.animate({scrollTop: scroll_pos});
|
||||
}
|
||||
});
|
||||
|
||||
$('#resources-xml').on('tripleclick', function () {
|
||||
openPlexXML('/api/resources', true, {includeHttps: 1});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
68
data/interfaces/default/support.html
Normal file
68
data/interfaces/default/support.html
Normal file
@@ -0,0 +1,68 @@
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
from plexpy.helpers import anon_url
|
||||
%>
|
||||
<%def name="headIncludes()">
|
||||
</%def>
|
||||
|
||||
<%def name="headerIncludes()">
|
||||
</%def>
|
||||
|
||||
<%def name="body()">
|
||||
<div class='container-fluid'>
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-comment"></i> Support</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-dark" href="${anon_url('https://tautulli.com/discord')}" target="_blank"><i class="fab fa-discord"></i> Join Discord</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<a class="btn btn-dark" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank"><i class="fab fa-reddit"></i> Join Reddit</a>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-dark" id="popout-iframe-button"><i class="fa fa-external-link"></i> Pop Out Chat</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<div class="iframe-container">
|
||||
<iframe class="iframe" allowfullscreen="true" id="support-iframe" data-name="Tautulli-Support" data-src="https://support.tautulli.com"
|
||||
sandbox="allow-presentation allow-forms allow-same-origin allow-pointer-lock allow-scripts allow-popups allow-modals allow-top-navigation"
|
||||
style="display: none;">
|
||||
</iframe>
|
||||
<div class="iframe-overlay">
|
||||
<div class="iframe-button-container">
|
||||
<a class="iframe-button">Start chatting now</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
<%def name="modalIncludes()">
|
||||
</%def>
|
||||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script>
|
||||
var popout_chat;
|
||||
$('.iframe-button').click(function () {
|
||||
if (popout_chat) {
|
||||
popout_chat.close();
|
||||
}
|
||||
|
||||
var iframe = $('#support-iframe');
|
||||
iframe.attr('src', iframe.data('src')).fadeIn();
|
||||
$('.iframe-overlay').fadeOut();
|
||||
});
|
||||
|
||||
$('#popout-iframe-button').click(function () {
|
||||
var iframe = $('#support-iframe');
|
||||
popout_chat = window.open(iframe.data('src'), 'Tautulli-Discord-Support', 'width=1280,height=720');
|
||||
iframe.attr('src', '').fadeOut();
|
||||
$('.iframe-overlay').fadeIn();
|
||||
});
|
||||
</script>
|
||||
</%def>
|
@@ -16,7 +16,7 @@
|
||||
<div class='container-fluid'>
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-cloud-download"></i> Synced Items</span>
|
||||
<span id="sync-xml"><i class="fa fa-cloud-download"></i> Synced Items</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -185,5 +185,9 @@
|
||||
$("#refresh-syncs-list").click(function() {
|
||||
sync_table.ajax.reload();
|
||||
});
|
||||
|
||||
$('#sync-xml').on('tripleclick', function () {
|
||||
openPlexXML('/servers/{machine_id}/sync_lists', true);
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
@@ -10,7 +10,7 @@
|
||||
<div class='container-fluid'>
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-group"></i> All Users</span>
|
||||
<span id="users-xml"><i class="fa fa-group"></i> All Users</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
% if _session['user_group'] == 'admin':
|
||||
@@ -202,5 +202,9 @@
|
||||
});
|
||||
});
|
||||
% endif
|
||||
|
||||
$('#users-xml').on('tripleclick', function () {
|
||||
openPlexXML('/api/users', true);
|
||||
});
|
||||
</script>
|
||||
</%def>
|
@@ -42,6 +42,7 @@ import datafactory
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import newsletters
|
||||
import newsletter_handler
|
||||
import notification_handler
|
||||
import notifiers
|
||||
@@ -202,6 +203,7 @@ def initialize(config_file):
|
||||
logger.error(u"Could not perform upgrades: %s" % e)
|
||||
|
||||
# Add notifier configs to logger blacklist
|
||||
newsletters.blacklist_logger()
|
||||
notifiers.blacklist_logger()
|
||||
mobile_app.blacklist_logger()
|
||||
|
||||
@@ -516,7 +518,7 @@ def dbcheck():
|
||||
|
||||
# sessions table :: This is a temp table that logs currently active sessions
|
||||
c_db.execute(
|
||||
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, '
|
||||
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, session_id TEXT, '
|
||||
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
|
||||
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
|
||||
'ip_address TEXT, machine_id TEXT, player TEXT, product TEXT, platform TEXT, title TEXT, parent_title TEXT, '
|
||||
@@ -932,7 +934,7 @@ def dbcheck():
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN product INTEGER'
|
||||
'ALTER TABLE sessions ADD COLUMN product TEXT'
|
||||
)
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN optimized_version INTEGER'
|
||||
@@ -1079,6 +1081,15 @@ def dbcheck():
|
||||
'ALTER TABLE sessions ADD COLUMN live_uuid TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT session_id FROM sessions')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE sessions ADD COLUMN session_id TEXT'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT original_title FROM sessions')
|
||||
|
@@ -15,18 +15,13 @@
|
||||
|
||||
from collections import defaultdict
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
import re
|
||||
|
||||
import plexpy
|
||||
import database
|
||||
import datafactory
|
||||
import helpers
|
||||
import libraries
|
||||
import log_reader
|
||||
import logger
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import pmsconnect
|
||||
import users
|
||||
|
||||
@@ -39,6 +34,7 @@ class ActivityProcessor(object):
|
||||
def write_session(self, session=None, notify=True):
|
||||
if session:
|
||||
values = {'session_key': session.get('session_key', ''),
|
||||
'session_id': session.get('session_id', ''),
|
||||
'transcode_key': session.get('transcode_key', ''),
|
||||
'section_id': session.get('section_id', ''),
|
||||
'rating_key': session.get('rating_key', ''),
|
||||
@@ -272,14 +268,15 @@ class ActivityProcessor(object):
|
||||
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
|
||||
|
||||
# Check if we should group the session, select the last two rows from the user
|
||||
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history \
|
||||
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
|
||||
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history ' \
|
||||
'WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 '
|
||||
|
||||
args = [session['user_id']]
|
||||
args = [session['user_id'], session['rating_key']]
|
||||
|
||||
result = self.db.select(query=query, args=args)
|
||||
|
||||
new_session = prev_session = None
|
||||
prev_progress_percent = media_watched_percent = 0
|
||||
# Get the last insert row id
|
||||
last_id = self.db.last_insert_id()
|
||||
|
||||
@@ -296,11 +293,23 @@ class ActivityProcessor(object):
|
||||
'user_id': result[1]['user_id'],
|
||||
'reference_id': result[1]['reference_id']}
|
||||
|
||||
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
|
||||
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
|
||||
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT
|
||||
}
|
||||
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
|
||||
media_watched_percent = watched_percent.get(session['media_type'], 0)
|
||||
|
||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new 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 prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
|
||||
elif prev_progress_percent < media_watched_percent and \
|
||||
prev_session['view_offset'] <= new_session['view_offset']:
|
||||
args = [prev_session['reference_id'], new_session['id']]
|
||||
else:
|
||||
args = [new_session['id'], new_session['id']]
|
||||
@@ -458,6 +467,16 @@ class ActivityProcessor(object):
|
||||
|
||||
return None
|
||||
|
||||
def get_session_by_id(self, session_id=None):
|
||||
if session_id:
|
||||
session = self.db.select_single('SELECT * FROM sessions '
|
||||
'WHERE session_id = ? ',
|
||||
args=[session_id])
|
||||
if session:
|
||||
return session
|
||||
|
||||
return None
|
||||
|
||||
def set_session_state(self, session_key=None, state=None, **kwargs):
|
||||
if str(session_key).isdigit():
|
||||
values = {}
|
||||
|
@@ -23,6 +23,7 @@ PLATFORM = platform.system()
|
||||
PLATFORM_RELEASE = platform.release()
|
||||
PLATFORM_VERSION = platform.version()
|
||||
PLATFORM_LINUX_DISTRO = ' '.join(x for x in platform.linux_distribution() if x)
|
||||
PLATFORM_DEVICE_NAME = platform.node()
|
||||
BRANCH = version.PLEXPY_BRANCH
|
||||
RELEASE = version.PLEXPY_RELEASE_VERSION
|
||||
|
||||
@@ -306,10 +307,17 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Server Version', 'type': 'str', 'value': 'server_version', 'description': 'The current version of your Plex Server.'},
|
||||
{'name': 'Server ID', 'type': 'str', 'value': 'server_machine_id', 'description': 'The unique identifier for your Plex Server.'},
|
||||
{'name': 'Action', 'type': 'str', 'value': 'action', 'description': 'The action that triggered the notification.'},
|
||||
{'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year when the notfication was triggered.'},
|
||||
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification was triggered.'},
|
||||
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification was triggered.'},
|
||||
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification was triggered.'},
|
||||
{'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year when the notfication is triggered.'},
|
||||
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month when the notfication is triggered.', 'example': '1 to 12'},
|
||||
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day when the notfication is triggered.', 'example': '1 to 31'},
|
||||
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour when the notfication is triggered.', 'example': '0 to 23'},
|
||||
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute when the notfication is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second when the notfication is triggered.', 'example': '0 to 59'},
|
||||
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday when the notfication is triggered.', 'example': '1 (Mon) to 7 (Sun)'},
|
||||
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number when the notfication is triggered.', 'example': '1 to 52'},
|
||||
{'name': 'Datestamp', 'type': 'str', 'value': 'datestamp', 'description': 'The date (in date format) when the notification is triggered.'},
|
||||
{'name': 'Timestamp', 'type': 'str', 'value': 'timestamp', 'description': 'The time (in time format) when the notification is triggered.'},
|
||||
{'name': 'Unix Time', 'type': 'int', 'value': 'unixtime', 'description': 'The unix timestamp when the notification is triggered.'},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -395,7 +403,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{
|
||||
'category': 'Source Metadata Details',
|
||||
'parameters': [
|
||||
{'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', 'example': 'movie, show, season, episode, artist, album, track'},
|
||||
{'name': 'Media Type', 'type': 'str', 'value': 'media_type', 'description': 'The type of media.', 'example': 'movie, show, season, episode, artist, album, track, clip'},
|
||||
{'name': 'Title', 'type': 'str', 'value': 'title', 'description': 'The full title of the item.'},
|
||||
{'name': 'Library Name', 'type': 'str', 'value': 'library_name', 'description': 'The library name of the item.'},
|
||||
{'name': 'Show Name', 'type': 'str', 'value': 'show_name', 'description': 'The title of the TV series.'},
|
||||
@@ -475,6 +483,7 @@ NOTIFICATION_PARAMETERS = [
|
||||
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'},
|
||||
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
|
||||
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
|
||||
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
|
||||
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
|
||||
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
|
||||
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},
|
||||
@@ -525,7 +534,14 @@ NEWSLETTER_PARAMETERS = [
|
||||
{'name': 'Server Name', 'type': 'str', 'value': 'server_name', 'description': 'The name of your Plex Server.'},
|
||||
{'name': 'Start Date', 'type': 'str', 'value': 'start_date', 'description': 'The start date of the newsletter.'},
|
||||
{'name': 'End Date', 'type': 'str', 'value': 'end_date', 'description': 'The end date of the newsletter.'},
|
||||
{'name': 'Week Number', 'type': 'int', 'value': 'week_number', 'description': 'The week number of the year.'},
|
||||
{'name': 'Current Year', 'type': 'int', 'value': 'current_year', 'description': 'The year of the start date of the newsletter.'},
|
||||
{'name': 'Current Month', 'type': 'int', 'value': 'current_month', 'description': 'The month of the start date of the newsletter.', 'example': '1 to 12'},
|
||||
{'name': 'Current Day', 'type': 'int', 'value': 'current_day', 'description': 'The day of the start date of the newsletter.', 'example': '1 to 31'},
|
||||
{'name': 'Current Hour', 'type': 'int', 'value': 'current_hour', 'description': 'The hour of the start date of the newsletter.', 'example': '0 to 23'},
|
||||
{'name': 'Current Minute', 'type': 'int', 'value': 'current_minute', 'description': 'The minute of the start date of the newsletter.', 'example': '0 to 59'},
|
||||
{'name': 'Current Second', 'type': 'int', 'value': 'current_second', 'description': 'The second of the start date of the newsletter.', 'example': '0 to 59'},
|
||||
{'name': 'Current Weekday', 'type': 'int', 'value': 'current_weekday', 'description': 'The ISO weekday of the start date of the newsletter.', 'example': '1 (Mon) to 7 (Sun)'},
|
||||
{'name': 'Current Week', 'type': 'int', 'value': 'current_week', 'description': 'The ISO week number of the start date of the newsletter.', 'example': '1 to 52'},
|
||||
{'name': 'Newsletter Time Frame', 'type': 'int', 'value': 'newsletter_time_frame', 'description': 'The time frame included in the newsletter.'},
|
||||
{'name': 'Newsletter Time Frame Units', 'type': 'str', 'value': 'newsletter_time_frame_units', 'description': 'The time frame units included in the newsletter.'},
|
||||
{'name': 'Newsletter URL', 'type': 'str', 'value': 'newsletter_url', 'description': 'The self-hosted URL to the newsletter.'},
|
||||
|
@@ -54,6 +54,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PMS_TOKEN': (str, 'PMS', ''),
|
||||
'PMS_SSL': (int, 'PMS', 0),
|
||||
'PMS_URL': (str, 'PMS', ''),
|
||||
'PMS_URL_OVERRIDE': (str, 'PMS', ''),
|
||||
'PMS_URL_MANUAL': (int, 'PMS', 0),
|
||||
'PMS_USE_BIF': (int, 'PMS', 0),
|
||||
'PMS_UUID': (str, 'PMS', ''),
|
||||
|
@@ -65,7 +65,7 @@ class DataFactory(object):
|
||||
columns = [
|
||||
'session_history.reference_id',
|
||||
'session_history.id',
|
||||
'started AS date',
|
||||
'MAX(started) AS date',
|
||||
'MIN(started) AS started',
|
||||
'MAX(stopped) AS stopped',
|
||||
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \
|
||||
@@ -1467,7 +1467,7 @@ class DataFactory(object):
|
||||
result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'),
|
||||
args=[item['parent_rating_key']])
|
||||
for item in result:
|
||||
key = item['media_index']
|
||||
key = item['media_index'] if item['media_index'] else item['title']
|
||||
children.update({key: {'rating_key': item['rating_key']}})
|
||||
|
||||
key = item['parent_media_index'] if match_type == 'index' else item['parent_title']
|
||||
|
@@ -50,7 +50,9 @@ class Graphs(object):
|
||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'GROUP BY date(started, "unixepoch", "localtime"), %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
'GROUP BY date_played ' \
|
||||
'ORDER BY started ASC' % (group_by, time_range, user_cond)
|
||||
@@ -147,7 +149,9 @@ class Graphs(object):
|
||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'GROUP BY strftime("%%w", datetime(started, "unixepoch", "localtime")), %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
'GROUP BY dayofweek ' \
|
||||
'ORDER BY daynumber' % (group_by, time_range, user_cond)
|
||||
@@ -245,7 +249,9 @@ class Graphs(object):
|
||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'GROUP BY strftime("%%H", datetime(started, "unixepoch", "localtime")) , %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
|
||||
'GROUP BY hourofday ' \
|
||||
'ORDER BY hourofday' % (group_by, time_range, user_cond)
|
||||
@@ -335,7 +341,9 @@ class Graphs(object):
|
||||
'SUM(CASE WHEN media_type = "episode" THEN 1 ELSE 0 END) AS tv_count, ' \
|
||||
'SUM(CASE WHEN media_type = "movie" THEN 1 ELSE 0 END) AS movie_count, ' \
|
||||
'SUM(CASE WHEN media_type = "track" THEN 1 ELSE 0 END) AS music_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")), %s) ' \
|
||||
'AS session_history ' \
|
||||
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
|
||||
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
|
||||
'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
|
||||
@@ -591,7 +599,9 @@ class Graphs(object):
|
||||
'THEN 1 ELSE 0 END) AS ds_count, ' \
|
||||
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
|
||||
'THEN 1 ELSE 0 END) AS tc_count ' \
|
||||
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
|
||||
'FROM (SELECT * FROM session_history ' \
|
||||
'GROUP BY date(session_history.started, "unixepoch", "localtime"), %s) ' \
|
||||
'AS session_history ' \
|
||||
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
|
||||
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
|
||||
'datetime("now", "-%s days", "localtime")) AND ' \
|
||||
|
@@ -39,12 +39,13 @@ class HTTPHandler(object):
|
||||
else:
|
||||
self.urls = urls
|
||||
|
||||
self.headers = {'X-Plex-Device-Name': 'Tautulli',
|
||||
'X-Plex-Product': 'Tautulli',
|
||||
self.headers = {'X-Plex-Product': 'Tautulli',
|
||||
'X-Plex-Version': plexpy.common.RELEASE,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
'X-Plex-Platform': plexpy.common.PLATFORM,
|
||||
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
|
||||
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
|
||||
'X-Plex-Device': 'Web',
|
||||
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
|
||||
}
|
||||
|
||||
self.token = token
|
||||
@@ -178,5 +179,5 @@ class HTTPHandler(object):
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.response_type, e))
|
||||
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.output_format, e))
|
||||
return None
|
||||
|
@@ -28,6 +28,7 @@ import traceback
|
||||
|
||||
import plexpy
|
||||
import helpers
|
||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||
|
||||
# These settings are for file logging only
|
||||
FILENAME = "tautulli.log"
|
||||
@@ -48,6 +49,20 @@ logger_plex_websocket = logging.getLogger("plex_websocket")
|
||||
# Global queue for multiprocessing logging
|
||||
queue = None
|
||||
|
||||
|
||||
def blacklist_config(config):
|
||||
blacklist = set()
|
||||
blacklist_keys = ['HOOK', 'APIKEY', 'KEY', 'PASSWORD', 'TOKEN']
|
||||
|
||||
for key, value in config.iteritems():
|
||||
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
||||
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
|
||||
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
|
||||
blacklist.add(value.strip())
|
||||
|
||||
_BLACKLIST_WORDS.update(blacklist)
|
||||
|
||||
|
||||
class NoThreadFilter(logging.Filter):
|
||||
"""
|
||||
Log filter for the current thread
|
||||
|
@@ -138,7 +138,5 @@ def set_last_seen(device_token=None):
|
||||
|
||||
def blacklist_logger():
|
||||
devices = get_mobile_devices()
|
||||
|
||||
blacklist = set(d['device_token'] for d in devices)
|
||||
|
||||
logger._BLACKLIST_WORDS.update(blacklist)
|
||||
for d in devices:
|
||||
logger.blacklist_config(d)
|
||||
|
@@ -196,6 +196,7 @@ def add_newsletter_config(agent_id=None, **kwargs):
|
||||
newsletter_id = db.last_insert_id()
|
||||
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
|
||||
% (agent['label'], newsletter_id))
|
||||
blacklist_logger()
|
||||
return newsletter_id
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
|
||||
@@ -254,6 +255,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
|
||||
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
|
||||
% (agent['label'], newsletter_id))
|
||||
newsletter_handler.schedule_newsletters(newsletter_id=newsletter_id)
|
||||
blacklist_logger()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
|
||||
@@ -274,6 +276,17 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
|
||||
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
|
||||
|
||||
|
||||
def blacklist_logger():
|
||||
db = database.MonitorDatabase()
|
||||
notifiers = db.select('SELECT newsletter_config, email_config FROM newsletters')
|
||||
|
||||
for n in notifiers:
|
||||
config = json.loads(n['newsletter_config'] or '{}')
|
||||
logger.blacklist_config(config)
|
||||
email_config = json.loads(n['email_config'] or '{}')
|
||||
logger.blacklist_config(email_config)
|
||||
|
||||
|
||||
def serve_template(templatename, **kwargs):
|
||||
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
|
||||
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR
|
||||
@@ -542,6 +555,14 @@ class Newsletter(object):
|
||||
'server_name': plexpy.CONFIG.PMS_NAME,
|
||||
'start_date': self.start_date.format(date_format),
|
||||
'end_date': self.end_date.format(date_format),
|
||||
'current_year': self.start_date.year,
|
||||
'current_month': self.start_date.month,
|
||||
'current_day': self.start_date.day,
|
||||
'current_hour': self.start_date.hour,
|
||||
'current_minute': self.start_date.minute,
|
||||
'current_second': self.start_date.second,
|
||||
'current_weekday': self.start_date.isocalendar()[2],
|
||||
'current_week': self.start_date.isocalendar()[1],
|
||||
'week_number': self.start_date.isocalendar()[1],
|
||||
'newsletter_time_frame': self.config['time_frame'],
|
||||
'newsletter_time_frame_units': self.config['time_frame_units'],
|
||||
|
@@ -701,6 +701,9 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
child_count = 1
|
||||
grandchild_count = 1
|
||||
|
||||
now = arrow.now()
|
||||
now_iso = now.isocalendar()
|
||||
|
||||
available_params = {
|
||||
# Global paramaters
|
||||
'tautulli_version': common.RELEASE,
|
||||
@@ -715,9 +718,17 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'server_platform': plexpy.CONFIG.PMS_PLATFORM,
|
||||
'server_version': plexpy.CONFIG.PMS_VERSION,
|
||||
'action': notify_action.split('on_')[-1],
|
||||
'week_number': arrow.now().isocalendar()[1],
|
||||
'datestamp': arrow.now().format(date_format),
|
||||
'timestamp': arrow.now().format(time_format),
|
||||
'current_year': now.year,
|
||||
'current_month': now.month,
|
||||
'current_day': now.day,
|
||||
'current_hour': now.hour,
|
||||
'current_minute': now.minute,
|
||||
'current_second': now.second,
|
||||
'current_weekday': now_iso[2],
|
||||
'current_week': now_iso[1],
|
||||
'week_number': now_iso[1], # Keep for backwards compatibility
|
||||
'datestamp': now.format(date_format),
|
||||
'timestamp': now.format(time_format),
|
||||
'unixtime': int(time.time()),
|
||||
# Stream parameters
|
||||
'streams': stream_count,
|
||||
@@ -883,6 +894,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
'subtitle_language': notify_params['subtitle_language'],
|
||||
'subtitle_language_code': notify_params['subtitle_language_code'],
|
||||
'file': notify_params['file'],
|
||||
'filename': os.path.basename(notify_params['file']),
|
||||
'file_size': helpers.humanFileSize(notify_params['file_size']),
|
||||
'indexes': notify_params['indexes'],
|
||||
'section_id': notify_params['section_id'],
|
||||
@@ -908,6 +920,9 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
||||
pms_download_info = defaultdict(str, kwargs.pop('pms_download_info', {}))
|
||||
plexpy_download_info = defaultdict(str, kwargs.pop('plexpy_download_info', {}))
|
||||
|
||||
now = arrow.now()
|
||||
now_iso = now.isocalendar()
|
||||
|
||||
available_params = {
|
||||
# Global paramaters
|
||||
'tautulli_version': common.RELEASE,
|
||||
@@ -922,8 +937,17 @@ def build_server_notify_params(notify_action=None, **kwargs):
|
||||
'server_version': plexpy.CONFIG.PMS_VERSION,
|
||||
'server_machine_id': plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
'action': notify_action.split('on_')[-1],
|
||||
'datestamp': arrow.now().format(date_format),
|
||||
'timestamp': arrow.now().format(time_format),
|
||||
'current_year': now.year,
|
||||
'current_month': now.month,
|
||||
'current_day': now.day,
|
||||
'current_hour': now.hour,
|
||||
'current_minute': now.minute,
|
||||
'current_second': now.second,
|
||||
'current_weekday': now_iso[2],
|
||||
'current_week': now_iso[1],
|
||||
'week_number': now_iso[1], # Keep for backwards compatibility
|
||||
'datestamp': now.format(date_format),
|
||||
'timestamp': now.format(time_format),
|
||||
'unixtime': int(time.time()),
|
||||
# Plex Media Server update parameters
|
||||
'update_version': pms_download_info['version'],
|
||||
|
@@ -62,7 +62,6 @@ import mobile_app
|
||||
import pmsconnect
|
||||
import request
|
||||
import users
|
||||
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
|
||||
|
||||
|
||||
BROWSER_NOTIFIERS = {}
|
||||
@@ -612,17 +611,9 @@ def blacklist_logger():
|
||||
db = database.MonitorDatabase()
|
||||
notifiers = db.select('SELECT notifier_config FROM notifiers')
|
||||
|
||||
blacklist = set()
|
||||
blacklist_keys = ['hook', 'key', 'password', 'token']
|
||||
|
||||
for n in notifiers:
|
||||
config = json.loads(n['notifier_config'] or '{}')
|
||||
for key, value in config.iteritems():
|
||||
if isinstance(value, basestring) and len(value.strip()) > 5 and \
|
||||
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
|
||||
blacklist.add(value.strip())
|
||||
|
||||
logger._BLACKLIST_WORDS.update(blacklist)
|
||||
logger.blacklist_config(config)
|
||||
|
||||
|
||||
class PrettyMetadata(object):
|
||||
@@ -682,13 +673,13 @@ class PrettyMetadata(object):
|
||||
provider_name = 'Trakt.tv'
|
||||
elif provider == 'lastfm':
|
||||
provider_name = 'Last.fm'
|
||||
else:
|
||||
if self.media_type == 'movie':
|
||||
provider_name = 'IMDb'
|
||||
elif self.media_type in ('show', 'season', 'episode'):
|
||||
provider_name = 'TheTVDB'
|
||||
elif self.media_type in ('artist', 'album', 'track'):
|
||||
provider_name = 'Last.fm'
|
||||
# else:
|
||||
# if self.media_type == 'movie':
|
||||
# provider_name = 'IMDb'
|
||||
# elif self.media_type in ('show', 'season', 'episode'):
|
||||
# provider_name = 'TheTVDB'
|
||||
# elif self.media_type in ('artist', 'album', 'track'):
|
||||
# provider_name = 'Last.fm'
|
||||
return provider_name
|
||||
|
||||
def get_provider_link(self, provider=None):
|
||||
@@ -697,13 +688,13 @@ class PrettyMetadata(object):
|
||||
provider_link = self.get_plex_url()
|
||||
elif provider:
|
||||
provider_link = self.parameters.get(provider + '_url', '')
|
||||
else:
|
||||
if self.media_type == 'movie':
|
||||
provider_link = self.parameters.get('imdb_url', '')
|
||||
elif self.media_type in ('show', 'season', 'episode'):
|
||||
provider_link = self.parameters.get('thetvdb_url', '')
|
||||
elif self.media_type in ('artist', 'album', 'track'):
|
||||
provider_link = self.parameters.get('lastfm_url', '')
|
||||
# else:
|
||||
# if self.media_type == 'movie':
|
||||
# provider_link = self.parameters.get('imdb_url', '')
|
||||
# elif self.media_type in ('show', 'season', 'episode'):
|
||||
# provider_link = self.parameters.get('thetvdb_url', '')
|
||||
# elif self.media_type in ('artist', 'album', 'track'):
|
||||
# provider_link = self.parameters.get('lastfm_url', '')
|
||||
return provider_link
|
||||
|
||||
def get_caption(self, provider):
|
||||
@@ -711,6 +702,7 @@ class PrettyMetadata(object):
|
||||
return 'View on ' + provider_name
|
||||
|
||||
def get_title(self, divider='-'):
|
||||
title = ''
|
||||
if self.media_type == 'movie':
|
||||
title = '%s (%s)' % (self.parameters['title'], self.parameters['year'])
|
||||
elif self.media_type == 'show':
|
||||
@@ -1251,7 +1243,7 @@ class DISCORD(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'discord_movie_provider',
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -1259,7 +1251,7 @@ class DISCORD(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'discord_tv_provider',
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -1267,7 +1259,7 @@ class DISCORD(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'discord_music_provider',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
@@ -1600,7 +1592,7 @@ class FACEBOOK(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'facebook_movie_provider',
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -1608,7 +1600,7 @@ class FACEBOOK(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'facebook_tv_provider',
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -1616,7 +1608,7 @@ class FACEBOOK(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'facebook_music_provider',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
@@ -1936,7 +1928,7 @@ class HIPCHAT(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'hipchat_movie_provider',
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -1944,7 +1936,7 @@ class HIPCHAT(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'hipchat_tv_provider',
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -1952,7 +1944,7 @@ class HIPCHAT(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'hipchat_music_provider',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
@@ -2144,7 +2136,7 @@ class JOIN(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'join_movie_provider',
|
||||
'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -2152,7 +2144,7 @@ class JOIN(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'join_tv_provider',
|
||||
'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -2160,7 +2152,7 @@ class JOIN(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'join_music_provider',
|
||||
'description': 'Select the source for music links in the notificaation. Leave blank for default.',
|
||||
'description': 'Select the source for music links in the notification. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
@@ -2450,7 +2442,8 @@ class PLEX(Notifier):
|
||||
else:
|
||||
return request.request_content(url)
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
def _sendjson(self, host, method, params=None):
|
||||
params = params or {}
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
@@ -2928,7 +2921,7 @@ class PUSHOVER(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'pushover_movie_provider',
|
||||
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -2936,7 +2929,7 @@ class PUSHOVER(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'pushover_tv_provider',
|
||||
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -2944,7 +2937,7 @@ class PUSHOVER(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'pushover_music_provider',
|
||||
'description': 'Select the source for music links in the notification. Leave blank for default.',
|
||||
'description': 'Select the source for music links in the notification. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
@@ -3303,7 +3296,7 @@ class SLACK(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'slack_movie_provider',
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -3311,7 +3304,7 @@ class SLACK(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'slack_tv_provider',
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -3319,7 +3312,7 @@ class SLACK(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'slack_music_provider',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank for default.',
|
||||
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
@@ -3553,7 +3546,8 @@ class XBMC(Notifier):
|
||||
else:
|
||||
return request.request_content(url)
|
||||
|
||||
def _sendjson(self, host, method, params={}):
|
||||
def _sendjson(self, host, method, params=None):
|
||||
params = params or {}
|
||||
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
|
||||
headers = {'Content-Type': 'application/json'}
|
||||
url = host + '/jsonrpc'
|
||||
@@ -3714,7 +3708,7 @@ class ZAPIER(Notifier):
|
||||
{'label': 'Movie Link Source',
|
||||
'value': self.config['movie_provider'],
|
||||
'name': 'zapier_movie_provider',
|
||||
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
|
||||
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_movie_providers()
|
||||
@@ -3722,7 +3716,7 @@ class ZAPIER(Notifier):
|
||||
{'label': 'TV Show Link Source',
|
||||
'value': self.config['tv_provider'],
|
||||
'name': 'zapier_tv_provider',
|
||||
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
|
||||
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
|
||||
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_tv_providers()
|
||||
@@ -3730,7 +3724,7 @@ class ZAPIER(Notifier):
|
||||
{'label': 'Music Link Source',
|
||||
'value': self.config['music_provider'],
|
||||
'name': 'zapier_music_provider',
|
||||
'description': 'Select the source for music links in the notification. Leave blank for default.',
|
||||
'description': 'Select the source for music links in the notification. Leave blank to disable.',
|
||||
'input_type': 'select',
|
||||
'select_options': PrettyMetadata().get_music_providers()
|
||||
}
|
||||
|
@@ -19,6 +19,7 @@ import time
|
||||
import urllib
|
||||
|
||||
import plexpy
|
||||
import activity_processor
|
||||
import common
|
||||
import helpers
|
||||
import http_handler
|
||||
@@ -482,6 +483,7 @@ class PmsConnect(object):
|
||||
actors = []
|
||||
genres = []
|
||||
labels = []
|
||||
collections = []
|
||||
|
||||
if m.getElementsByTagName('Director'):
|
||||
for director in m.getElementsByTagName('Director'):
|
||||
@@ -503,6 +505,10 @@ class PmsConnect(object):
|
||||
for label in m.getElementsByTagName('Label'):
|
||||
labels.append(helpers.get_xml_attr(label, 'tag'))
|
||||
|
||||
if m.getElementsByTagName('Collection'):
|
||||
for collection in m.getElementsByTagName('Collection'):
|
||||
collections.append(helpers.get_xml_attr(collection, 'tag'))
|
||||
|
||||
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
|
||||
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
|
||||
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
|
||||
@@ -540,6 +546,7 @@ class PmsConnect(object):
|
||||
'actors': actors,
|
||||
'genres': genres,
|
||||
'labels': labels,
|
||||
'collections': collections,
|
||||
'full_title': helpers.get_xml_attr(m, 'title'),
|
||||
'child_count': helpers.get_xml_attr(m, 'childCount')
|
||||
}
|
||||
@@ -583,6 +590,8 @@ class PmsConnect(object):
|
||||
metadata_xml = self.get_metadata(str(rating_key), output_format='xml')
|
||||
elif sync_id:
|
||||
metadata_xml = self.get_sync_item(str(sync_id), output_format='xml')
|
||||
else:
|
||||
return metadata
|
||||
|
||||
try:
|
||||
xml_head = metadata_xml.getElementsByTagName('MediaContainer')
|
||||
@@ -1044,7 +1053,7 @@ class PmsConnect(object):
|
||||
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': photo_album_details['banner'],
|
||||
'banner': photo_album_details.get('banner', ''),
|
||||
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
|
||||
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
|
||||
@@ -1053,10 +1062,10 @@ class PmsConnect(object):
|
||||
'directors': directors,
|
||||
'writers': writers,
|
||||
'actors': actors,
|
||||
'genres': photo_album_details['genres'],
|
||||
'labels': photo_album_details['labels'],
|
||||
'collections': photo_album_details['collections'],
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'genres': photo_album_details.get('genres', ''),
|
||||
'labels': photo_album_details.get('labels', ''),
|
||||
'collections': photo_album_details.get('collections', ''),
|
||||
'full_title': u'{} - {}'.format(helpers.get_xml_attr(metadata_main, 'parentTitle') or library_name,
|
||||
helpers.get_xml_attr(metadata_main, 'title')),
|
||||
'children_count': helpers.get_xml_attr(metadata_main, 'leafCount')
|
||||
}
|
||||
@@ -1153,7 +1162,7 @@ class PmsConnect(object):
|
||||
}
|
||||
|
||||
else:
|
||||
return {}
|
||||
return metadata
|
||||
|
||||
if metadata and media_info:
|
||||
medias = []
|
||||
@@ -1257,7 +1266,7 @@ class PmsConnect(object):
|
||||
|
||||
return metadata
|
||||
else:
|
||||
return {}
|
||||
return metadata
|
||||
|
||||
def get_metadata_children_details(self, rating_key='', get_children=False):
|
||||
"""
|
||||
@@ -1919,7 +1928,7 @@ class PmsConnect(object):
|
||||
|
||||
return session_output
|
||||
|
||||
def terminate_session(self, session_id='', message=''):
|
||||
def terminate_session(self, session_key='', session_id='', message=''):
|
||||
"""
|
||||
Terminates a streaming session.
|
||||
|
||||
@@ -1927,10 +1936,22 @@ class PmsConnect(object):
|
||||
"""
|
||||
message = message or 'The server owner has ended the stream.'
|
||||
|
||||
if session_key and not session_id:
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
session = ap.get_session_by_key(session_key=session_key)
|
||||
session_id = session['session_id']
|
||||
|
||||
elif session_id and not session_key:
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
session = ap.get_session_by_id(session_id=session_id)
|
||||
session_key = session['session_key']
|
||||
|
||||
if session_id:
|
||||
logger.info(u"Tautulli Pmsconnect :: Terminating session %s (session_id %s)." % (session_key, session_id))
|
||||
result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message))
|
||||
return result
|
||||
else:
|
||||
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session %s. Missing session_id." % session_key)
|
||||
return False
|
||||
|
||||
def get_item_children(self, rating_key='', get_grandchildren=False):
|
||||
@@ -2664,7 +2685,7 @@ class PmsConnect(object):
|
||||
child_title = helpers.get_xml_attr(item, 'title')
|
||||
|
||||
if child_rating_key:
|
||||
key = int(child_index)
|
||||
key = int(child_index) if child_index else child_title
|
||||
children.update({key: {'rating_key': int(child_rating_key)}})
|
||||
|
||||
key = int(parent_index) if match_type == 'index' else parent_title
|
||||
@@ -2676,9 +2697,9 @@ class PmsConnect(object):
|
||||
key = 0 if match_type == 'index' else title
|
||||
key_list = {key: {'rating_key': int(rating_key),
|
||||
'children': parents},
|
||||
'section_id': section_id,
|
||||
'library_name': library_name
|
||||
}
|
||||
'section_id': section_id,
|
||||
'library_name': library_name
|
||||
}
|
||||
|
||||
return key_list
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.10-beta"
|
||||
PLEXPY_BRANCH = "master"
|
||||
PLEXPY_RELEASE_VERSION = "v2.1.13"
|
||||
|
@@ -18,6 +18,7 @@ import json
|
||||
import os
|
||||
import shutil
|
||||
import threading
|
||||
import urllib
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib.static import serve_file, serve_download
|
||||
@@ -247,23 +248,23 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi()
|
||||
def terminate_session(self, session_id=None, message=None, **kwargs):
|
||||
""" Add a new notification agent.
|
||||
def terminate_session(self, session_key=None, session_id=None, message=None, **kwargs):
|
||||
""" Stop a streaming session.
|
||||
|
||||
```
|
||||
Required parameters:
|
||||
session_id (str): The id of the session to terminate
|
||||
message (str): A custom message to send to the client
|
||||
session_key (int): The session key of the session to terminate, OR
|
||||
session_id (str): The session id of the session to terminate
|
||||
|
||||
Optional parameters:
|
||||
None
|
||||
message (str): A custom message to send to the client
|
||||
|
||||
Returns:
|
||||
None
|
||||
```
|
||||
"""
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.terminate_session(session_id=session_id, message=message)
|
||||
result = pms_connect.terminate_session(session_key=session_key, session_id=session_id, message=message)
|
||||
|
||||
if result:
|
||||
return {'result': 'success', 'message': 'Session terminated.'}
|
||||
@@ -273,8 +274,21 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def return_sessions_url(self, **kwargs):
|
||||
return plexpy.CONFIG.PMS_URL + '/status/sessions?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN
|
||||
def return_plex_xml_url(self, endpoint='', plextv=False, **kwargs):
|
||||
kwargs['X-Plex-Token'] = plexpy.CONFIG.PMS_TOKEN
|
||||
|
||||
if plextv:
|
||||
base_url = 'https://plex.tv'
|
||||
else:
|
||||
if plexpy.CONFIG.PMS_URL_OVERRIDE:
|
||||
base_url = plexpy.CONFIG.PMS_URL_OVERRIDE
|
||||
else:
|
||||
base_url = plexpy.CONFIG.PMS_URL
|
||||
|
||||
if '{machine_id}' in endpoint:
|
||||
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
|
||||
|
||||
return base_url + endpoint + '?' + urllib.urlencode(kwargs)
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
@@ -5831,3 +5845,8 @@ class WebInterface(object):
|
||||
|
||||
logger.error(u"Failed to retrieve newsletter: Missing newsletter_id parameter.")
|
||||
return "Failed to retrieve newsletter: missing newsletter_id parameter"
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
def support(self, query='', **kwargs):
|
||||
return serve_template(templatename="support.html", title="Support")
|
||||
|
Reference in New Issue
Block a user