Compare commits

...

29 Commits

Author SHA1 Message Date
JonnyWong16
9d01335395 v2.0.5-beta 2017-12-31 17:52:53 -08:00
JonnyWong16
63814584e9 Add sortcut to /status/sessions 2017-12-31 17:37:07 -08:00
JonnyWong16
bf7e432c0c Add total bandwidth to activity header 2017-12-31 17:13:38 -08:00
JonnyWong16
5a14c229a1 Fix IPv6 overflow on current activity cards 2017-12-31 17:12:49 -08:00
JonnyWong16
79581fb83e Sanitize script output in logger 2017-12-31 14:41:32 -08:00
JonnyWong16
90c587e6e1 Unique session id name to not conflict with multiple installs 2017-12-31 14:33:21 -08:00
JonnyWong16
b2b728a3cc Save cherrypy sessions to disk instead of ram 2017-12-31 14:29:57 -08:00
JonnyWong16
645ef86c75 Fix Join notifications not sending 2017-12-31 14:29:57 -08:00
JonnyWong16
450fda18f1 Merge pull request #1181 from felixbuenemann/fix-cherrypy-ssl-connection-type-deprecation
Fix OpenSSL SSL.ConnectionType deprecation warning
2017-12-31 12:37:22 -07:00
Felix Bünemann
f6162a8df8 Fix OpenSSL SSL.ConnectionType deprecation warning
This patches the vendored cherrypy lib to work around a deprecation
warning in pyOpenSSL 17.1.0 and later by checking and using the
SSL.Connection class if available and falling back to ConnectionType.
2017-12-31 14:30:15 +01:00
JonnyWong16
a8c5f48b60 Merge branch 'nightly' of https://github.com/JonnyWong16/plexpy into nightly 2017-12-30 23:22:02 -08:00
JonnyWong16
edc0b96167 Some sync data updates 2017-12-30 23:21:31 -08:00
JonnyWong16
9b4b3e0ecb Add stopped state to history table activity 2017-12-30 17:27:59 -08:00
JonnyWong16
7bd2c00636 Fix presence in get_server_resources 2017-12-30 17:27:50 -08:00
JonnyWong16
15faccfa2f Update server connection code 2017-12-30 17:27:39 -08:00
JonnyWong16
11e10e7d0e Update resources links 2017-12-30 16:13:55 -08:00
JonnyWong16
8ef48fc548 Update svg logo with shadows 2017-12-30 14:14:47 -08:00
JonnyWong16
29632b0805 Validate git releases as a list 2017-12-29 16:49:55 -08:00
JonnyWong16
e908c45cf2 v2.0.4-beta 2017-12-29 16:35:24 -08:00
JonnyWong16
c37e934f42 Check concurrent streams from the server instead of database 2017-12-29 16:29:20 -08:00
JonnyWong16
41f91956b8 Make sure current activity is loaded before refreshing 2017-12-29 16:26:50 -08:00
JonnyWong16
8b2bd5ce79 Add collections info pages 2017-12-29 15:52:08 -08:00
JonnyWong16
28e4151157 Remember tab when going back on library and user pages 2017-12-29 11:34:41 -08:00
JonnyWong16
66d45293e6 Add test Plex Web URL button 2017-12-29 10:47:31 -08:00
JonnyWong16
243eeeff67 v2.0.3-beta 2017-12-25 09:58:24 -08:00
JonnyWong16
18520e24d1 Report correct release version on beta 2017-12-25 09:48:33 -08:00
JonnyWong16
87743171b7 Fix write session history optimized_version_title column name 2017-12-25 09:07:02 -08:00
JonnyWong16
452f0747c8 Add missing stream info modal css 2017-12-25 09:02:33 -08:00
JonnyWong16
a20747044c Fix sync ID 2017-12-25 09:00:42 -08:00
65 changed files with 1881 additions and 1289 deletions

View File

@@ -1,6 +1,44 @@
# Changelog
## v2.0.2-beta (2017-12-XX)
## v2.0.5-beta (2017-12-31)
* Monitoring:
* Fix: IPv6 addresses overflowing on the activity cards.
* Notifications:
* Fix: Error sending Join notifications.
* UI:
* New: Added total required bandwidth in the activity header.
* Fix: Failing to retrieve releases from GitHub.
* Other:
* Fix: CherryPy SSL connection warning. (Thanks @felixbuenemann)
* Fix: Sanitize script output in logs.
* Change: Login sessions persists across server restarts.
## v2.0.4-beta (2017-12-29)
* Monitoring:
* Fix: Current activity cards duplicating on the homepage.
* Notifications:
* Fix: Concurrent stream notifications being sent when there is an incorrect number of streams.
* UI:
* New: Info pages for collections.
* New: Button to test Plex Web URL override.
* Fix: Library and User pages return to the correct tab when pressing back.
## v2.0.3-beta (2017-12-25)
* Monitoring:
* Fix: Missing sync ID error causing logging to crash.
* Fix: Incorrect optimized version title column name causing logging to crash.
* Notifications:
* Fix: Report correct beta version for Tautulli update notifications.
* UI:
* Fix: Missing CSS for stream info modal.
## v2.0.2-beta (2017-12-24)
* Monitoring:
* Fix: Websocket connection fails to start with existing streams when upgrading to v2.

View File

@@ -20,26 +20,21 @@
${next.headIncludes()}
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.1">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.1">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.1">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS -->
<!-- Android -->
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.1">
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.1">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.1" color="#282a2d">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="initial-scale=1">
<meta name="format-detection" content="telephone=no">
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.1">
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head>
<body class="content">
@@ -69,8 +64,8 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="home">
<img alt="Tautulli" src="${http_root}images/logo-tautulli.svg" height="45">
<a class="navbar-brand svg" href="home">
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object>
</a>
</div>
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">

View File

@@ -74,11 +74,11 @@ DOCUMENTATION :: END
<tr>
<td class="top-line">Resources:</td>
<td class="top-line">
<a id="source-link" class="no-highlight" href="${anon_url('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Source</a> |
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Source</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/plexpy/issues' % plexpy.CONFIG.GIT_USER)}" data-id="issue">GitHub Issues</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" data-id="feature request">FeatHub Feature Requests</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">Tautulli Wiki</a> |
<a id="faq-source-link" class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank">Tautulli FAQ</a>
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Wiki &amp; FAQ</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" data-id="feature request">FeatHub Feature Requests</a> |
</td>
</tr>
<tr>
@@ -112,7 +112,6 @@ DOCUMENTATION :: END
$('.guidelines-modal-link').on('click', function (e) {
e.preventDefault();
$('#guidelines-link').attr('href', $('#source-link').attr('href'));
$('#guidelines-type').text($(this).data('id'))
$('#guidelines-modal').modal();
$('#guidelines-continue').attr('href', $(this).attr('href')).on('click', function () {
@@ -121,7 +120,6 @@ DOCUMENTATION :: END
});
$('.support-modal-link').on('click', function (e) {
e.preventDefault();
$('#faq-link').attr('href', $('#faq-source-link').attr('href'));
$('#support-modal').modal();
$('#support-continue').attr('href', $(this).attr('href')).on('click', function () {
$('#support-modal').modal('hide');

View File

@@ -13,6 +13,18 @@ a:focus {
text-decoration: none;
outline: none;
}
a.svg {
position: relative;
display: inline-block;
}
a.svg:after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
select, .react-selectize.bootstrap3.root-node .react-selectize-control {
margin: 5px 0 5px 0;
border: 2px solid #444;
@@ -841,6 +853,15 @@ a .users-poster-face:hover {
-webkit-flex-grow: 1;
flex-grow: 1;
}
.dashboard-activity-info-item .sub-value .ip-container {
display: inline-flex;
}
.dashboard-activity-info-item .sub-value .ip-address {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
}
.dashboard-activity-info-time {
position: absolute;
top: 200px;
@@ -1808,7 +1829,8 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
margin-left: 2px;
color: #999;
}
#children-list, #search-results-list {
.children-list,
.search-results-list {
position: relative;
z-index: 0;
}
@@ -1874,15 +1896,15 @@ a:hover .item-children-poster {
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
z-index: -2;
}
.item-children-poster-face.season-poster {
.item-children-poster-face.poster-item {
width: 150px;
height: 225px;
}
.item-children-poster-face.episode-poster {
.item-children-poster-face.episode-item {
width: 250px;
height: 140px;
}
.item-children-poster-face.album-poster {
.item-children-poster-face.cover-item {
width: 150px;
height: 150px;
}
@@ -1915,15 +1937,13 @@ a:hover .item-children-poster {
margin-bottom: 20px;
clear: both;
}
.item-children-instance-text-wrapper.season-item {
.item-children-instance-text-wrapper.poster-item,
.item-children-instance-text-wrapper.cover-item {
width: 150px;
}
.item-children-instance-text-wrapper.episode-item {
width: 250px;
}
.item-children-instance-text-wrapper.album-item {
width: 150px;
}
.item-children-instance-text-wrapper h3 {
width: 100%;
padding: 5px 3px 0 3px;
@@ -3698,4 +3718,75 @@ a:hover .overlay-refresh-image:hover {
}
.no-image {
background-image: none !important;
}
}
#info-modal .stream-info-item {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: baseline;
width: 100%;
margin-bottom: 5px;
}
#info-modal .stream-info-item .sub-heading {
height: 100%;
width: 75px;
color: #aaa;
font-size: 10px;
text-align: right;
text-transform: uppercase;
line-height: 14px;
-webkit-flex-shrink: 0;
flex-shrink: 0;
}
#info-modal .stream-info-item .sub-value {
color: #fff;
font-weight: bold;
margin-left: 10px;
text-align: left;
line-height: 14px;
-webkit-flex-grow: 1;
flex-grow: 1;
}
.stream-info {
width: 100%;
margin-top: 10px;
background-color: #282828;
table-layout: fixed;
}
.stream-info .heading {
color: #F9AA03;
text-transform: uppercase;
font-size: 15px;
font-weight: bold !important;
}
.stream-info th:first-child {
width: 125px;
height: 30px;
color: #fff;
font-size: 12px;
text-align: right;
text-transform: uppercase;
}
.stream-info th:not(:first-child) {
text-align: center;
font-weight: normal;
}
.stream-info td {
height: 25px;
}
.stream-info td:first-child {
color: #aaa;
font-size: 10px;
text-align: right;
text-transform: uppercase;
}
.stream-info td:not(:first-child) {
text-align: center;
}
.stream-info tr:nth-child(odd) td {
background-color: rgba(255,255,255,0.035);
}
.stream-info tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010);
}

View File

@@ -178,7 +178,7 @@ DOCUMENTATION :: END
<ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item">
% if _session['user_group'] == 'admin':
<div class="sub-heading"><span class="raw-stream-info-modal" data-toggle="modal" data-target="#raw-stream-info-modal" data-key="${sk}">Stream</span></div>\
<div class="sub-heading"><span class="raw-stream-info-modal" data-toggle="modal" data-target="#raw-stream-info-modal" data-key="${sk}">Stream</span></div>
% else:
<div class="sub-heading">Stream</div>
% endif
@@ -272,9 +272,9 @@ DOCUMENTATION :: END
<div class="sub-heading">Location</div>
<div class="sub-value">
% if data['ip_address'] != 'N/A':
${'LAN' if data['local'] == 1 else 'WAN'}: ${data['ip_address']}
${'LAN' if data['local'] == 1 else 'WAN'}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
</a>
<script>
isPrivateIP("${data['ip_address']}").then(function () {
@@ -301,7 +301,7 @@ DOCUMENTATION :: END
bw = str(bw) + ' kbps'
%>
<span id="stream-bandwidth-${sk}">${bw}</span>
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate"><i class="fa fa-info-circle"></i></span>
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
% elif data['synced_version'] == 1 or data['channel_stream'] == 1:
<span id="stream-bandwidth-${sk}">None</span>
% else:

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -2,9 +2,7 @@
<browserconfig>
<msapplication>
<tile>
<square70x70logo src="${http_root}images/favicon/mstile-70x70.png?v=2.0.1"/>
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.1"/>
<square310x310logo src="${http_root}images/favicon/mstile-310x310.png?v=2.0.1"/>
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.5"/>
<TileColor>#282a2d</TileColor>
</tile>
</msapplication>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 644 B

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,12 +2,12 @@
"name": "Tautulli",
"icons": [
{
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.1",
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.5",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.1",
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.5",
"sizes": "256x256",
"type": "image/png"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -2,69 +2,72 @@
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="900px"
height="300px" viewBox="-41.335 -35.197 900 300" enable-background="new -41.335 -35.197 900 300" xml:space="preserve">
height="300px" viewBox="0 0 900 300" enable-background="new 0 0 900 300" xml:space="preserve">
<g id="Layer_1">
<g id="Logo">
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="193" xlink:href="13AD46AC.png" transform="matrix(0.9547 0 0 0.9547 -17.8804 54.1814)">
</image>
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="258" xlink:href="13AD46AD.png" transform="matrix(0.9547 0 0 0.9584 -17.8804 -8.8303)">
</image>
<g>
<path fill="none" d="M401.343,203.396c7.493,0,14.836,0,22.407,0c-3.766-12.354-7.495-24.586-11.333-37.176
C408.665,178.818,405.017,191.064,401.343,203.396z"/>
<path fill="#FFFFFF" d="M436.772,189.627c-4.399-12.521-8.814-25.034-13.224-37.553c-7.552,0-15.105,0-22.657,0.002
c-0.14,0.611-0.221,1.246-0.428,1.836c-8.502,24.335-17.017,48.666-25.526,72.996c-1.45,4.146-2.887,8.298-4.362,12.535
c6.686,0,13.198,0,19.792,0c2.095-6.873,4.172-13.693,6.248-20.514c10.593,0,21.056,0,31.594,0
c2.102,6.894,4.177,13.707,6.245,20.489c6.66,0,13.146,0,19.772,0c-0.229-0.676-0.408-1.213-0.597-1.747
C448.014,221.656,442.4,205.639,436.772,189.627z M401.343,203.396c3.674-12.332,7.322-24.578,11.074-37.176
c3.837,12.59,7.567,24.82,11.333,37.176C416.179,203.396,408.836,203.396,401.343,203.396z"/>
<path fill="#FFFFFF" d="M371.072,152.459c-21.916,0-43.668,0-65.421,0c0,5.111,0,10.227,0,15.341c7.836,0,15.672,0,23.691,0
c0,24.046,0,47.786,0,71.621c6.164,0,12.144,0,18.361,0c0-23.901,0-47.698,0-71.682c7.948,0,15.66,0,23.369,0
C371.072,162.549,371.072,157.549,371.072,152.459z"/>
<path fill="#FFFFFF" d="M532.615,152.555c-6.176,0-12.154,0-18.349,0c0,0.842,0,1.592,0,2.34c0,16.42,0.021,32.838-0.021,49.258
c-0.006,2.675-0.119,5.373-0.494,8.018c-0.997,7.07-4.634,11.153-11.06,12.587c-3.731,0.83-7.491,0.754-11.238,0
c-5.745-1.157-9.344-4.668-10.58-10.297c-0.678-3.088-0.958-6.317-0.976-9.487c-0.094-16.738-0.045-33.478-0.045-50.217
c0-0.732,0-1.467,0-2.148c-6.296,0-12.275,0-18.329,0c0,0.591,0,1.035,0,1.48c0,18.209-0.013,36.416,0.018,54.625
c0.003,1.908,0.127,3.835,0.396,5.727c1.632,11.408,7.683,19.563,18.562,23.516c11.146,4.052,22.489,3.83,33.593-0.393
c7.937-3.017,13.483-8.627,16.483-16.617c1.474-3.93,2.044-8.024,2.042-12.211c-0.006-18.146-0.002-36.289-0.002-54.436
C532.615,153.744,532.615,153.188,532.615,152.555z"/>
<path fill="#FFFFFF" d="M688.532,152.555c-6.16,0-12.14,0-18.349,0c0,0.924,0,1.62,0,2.318c0,16.482,0.021,32.967-0.021,49.449
c-0.006,2.609-0.125,5.244-0.49,7.824c-1.003,7.127-4.74,11.291-11.226,12.654c-3.674,0.771-7.373,0.702-11.056-0.037
c-5.74-1.156-9.354-4.652-10.596-10.281c-0.682-3.086-0.964-6.317-0.979-9.487c-0.094-16.738-0.044-33.478-0.044-50.216
c0-0.739,0-1.478,0-2.18c-6.278,0-12.26,0-18.332,0c0,0.582,0,1.024,0,1.467c0.003,18.271-0.011,36.543,0.021,54.815
c0.002,1.847,0.132,3.707,0.391,5.533c1.626,11.411,7.669,19.567,18.546,23.533c11.213,4.09,22.621,3.836,33.771-0.445
c7.851-3.019,13.345-8.612,16.315-16.541c1.521-4.052,2.06-8.274,2.056-12.592c-0.018-17.953-0.008-35.905-0.008-53.858
C688.532,153.893,688.532,153.27,688.532,152.555z"/>
<path fill="#FFFFFF" d="M542.405,167.793c7.813,0,15.522,0,23.479,0c0,23.979,0,47.771,0,71.648c6.172,0,12.102,0,18.425,0
c0-23.948,0-47.738,0-71.729c7.947,0,15.655,0,23.378,0c0-5.158,0-10.115,0-15.115c-21.819,0-43.53,0-65.281,0
C542.405,157.691,542.405,162.648,542.405,167.793z"/>
<path fill="#FFFFFF" d="M725.407,152.58c-6.199,0-12.177,0-18.193,0c0,29.021,0,57.92,0,86.857c17.832,0,35.528,0,53.335,0
c0-5.191,0-10.146,0-15.307c-11.72,0-23.322,0-35.142,0C725.407,200.143,725.407,176.361,725.407,152.58z"/>
<path fill="#FFFFFF" d="M790.668,152.593c-6.197,0-12.172,0-18.216,0c0,28.985,0,57.833,0,86.831c17.905,0,35.6,0,53.371,0
c0-5.105,0-10.107,0-15.316c-11.729,0-23.335,0-35.155,0C790.668,200.119,790.668,176.339,790.668,152.593z"/>
<path fill="#FFFFFF" d="M838.183,239.464c6.083,0,12.021,0,18.096,0c0-29.09,0-57.987,0-86.879c-6.12,0-12.098,0-18.096,0
C838.183,181.611,838.183,210.514,838.183,239.464z"/>
<ellipse fill="#E5A00D" cx="847.229" cy="129.416" rx="12.459" ry="12.44"/>
</g>
</g>
<g id="Layer_2">
<g id="Layer_2_1_">
<g>
<path fill="none" d="M349.343,165.396c7.493,0,14.836,0,22.407,0c-3.766-12.354-7.495-24.585-11.333-37.175
C356.665,140.819,353.017,153.064,349.343,165.396z"/>
<path fill="#FFFFFF" d="M103.83,174.477c-7.6,0-14.441-3.232-19.236-8.394l-4.595,2.845l1.406,1.593
c-2.429,3.979-3.83,8.656-3.83,13.66c0,14.5,11.755,26.255,26.255,26.255c10.38,0,19.351-6.024,23.612-14.767l37.675,5.471
c0.22,14.31,11.881,25.845,26.244,25.845c14.5,0,26.254-11.755,26.254-26.254c0-14.5-11.754-26.254-26.254-26.254
c-10.438,0-19.453,6.092-23.685,14.915l-37.598-5.46c-0.064-6.86-2.759-13.089-7.125-17.73
C118.165,171.293,111.371,174.477,103.83,174.477z"/>
<path fill="#FFFFFF" d="M77.574,148.22c0-0.054,0.004-0.107,0.004-0.161l-41.163-46.637c3.078-4.301,4.895-9.564,4.895-15.257
c0-14.5-11.754-26.254-26.254-26.254c-14.5,0-26.254,11.754-26.254,26.254S0.556,112.42,15.056,112.42
c4.464,0,8.666-1.117,12.347-3.083l44.545,50.47l6.701-4.148C77.953,153.299,77.574,150.804,77.574,148.22z"/>
<path fill="#E5A00D" d="M191.361,49.885c14.5,0,26.254-11.754,26.254-26.254c0-14.499-11.754-26.254-26.254-26.254
s-26.254,11.755-26.254,26.254c0,7.145,2.858,13.619,7.488,18.353l-58.186,82.207c-3.237-1.427-6.814-2.226-10.58-2.226
c-14.446,0-26.165,11.669-26.252,26.095c0,0.054-0.004,0.107-0.004,0.161c0,2.584,0.379,5.078,1.075,7.438l-6.701,4.148
l-37.506,23.223c-4.801-5.255-11.708-8.552-19.386-8.552c-14.5,0-26.254,11.754-26.254,26.254
c0,14.499,11.754,26.254,26.254,26.254c14.5,0,26.254-11.755,26.254-26.254c0-2.506-0.358-4.926-1.014-7.221l39.703-24.582
l4.595-2.845c4.794,5.161,11.636,8.394,19.236,8.394c7.542,0,14.335-3.185,19.124-8.275c4.419-4.699,7.132-11.021,7.132-17.981
c0-6.396-2.29-12.257-6.092-16.812l58.736-82.983C185.434,49.367,188.336,49.885,191.361,49.885z"/>
<path fill="#FFFFFF" d="M384.772,151.627c-4.399-12.521-8.814-25.034-13.223-37.553c-7.552,0-15.106,0-22.657,0.002
c-0.14,0.612-0.221,1.246-0.428,1.836c-8.502,24.335-17.017,48.666-25.526,72.996c-1.451,4.146-2.887,8.298-4.363,12.536
c6.686,0,13.198,0,19.792,0c2.095-6.874,4.172-13.694,6.248-20.514c10.593,0,21.056,0,31.594,0
c2.102,6.893,4.177,13.707,6.245,20.489c6.66,0,13.147,0,19.773,0c-0.229-0.676-0.409-1.213-0.597-1.747
C396.014,183.656,390.4,167.638,384.772,151.627z M349.343,165.396c3.674-12.332,7.322-24.577,11.074-37.175
c3.837,12.59,7.567,24.82,11.333,37.175C364.179,165.396,356.836,165.396,349.343,165.396z"/>
<path fill="#FFFFFF" d="M319.072,114.459c-21.916,0-43.668,0-65.421,0c0,5.112,0,10.227,0,15.341c7.836,0,15.672,0,23.691,0
c0,24.046,0,47.786,0,71.621c6.164,0,12.144,0,18.361,0c0-23.902,0-47.698,0-71.682c7.948,0,15.66,0,23.369,0
C319.072,124.549,319.072,119.549,319.072,114.459z"/>
<path fill="#FFFFFF" d="M480.615,114.554c-6.176,0-12.154,0-18.348,0c0,0.843,0,1.593,0,2.341c0,16.42,0.021,32.838-0.021,49.258
c-0.006,2.674-0.119,5.373-0.494,8.017c-0.997,7.07-4.634,11.153-11.06,12.587c-3.731,0.83-7.491,0.754-11.238,0
c-5.745-1.157-9.344-4.668-10.58-10.297c-0.678-3.088-0.958-6.318-0.976-9.488c-0.093-16.737-0.044-33.477-0.044-50.216
c0-0.733,0-1.467,0-2.149c-6.296,0-12.276,0-18.33,0c0,0.591,0,1.036,0,1.481c0,18.209-0.012,36.416,0.018,54.625
c0.003,1.908,0.127,3.835,0.396,5.726c1.632,11.409,7.683,19.563,18.562,23.516c11.146,4.052,22.489,3.83,33.593-0.392
c7.936-3.017,13.483-8.627,16.483-16.618c1.474-3.93,2.044-8.024,2.042-12.21c-0.006-18.146-0.002-36.29-0.002-54.436
C480.615,115.744,480.615,115.187,480.615,114.554z"/>
<path fill="#FFFFFF" d="M636.532,114.554c-6.16,0-12.139,0-18.348,0c0,0.925,0,1.621,0,2.319c0,16.482,0.021,32.967-0.021,49.449
c-0.006,2.609-0.125,5.244-0.49,7.825c-1.003,7.126-4.74,11.291-11.226,12.654c-3.674,0.771-7.373,0.702-11.055-0.038
c-5.741-1.155-9.354-4.651-10.596-10.281c-0.682-3.086-0.964-6.317-0.979-9.487c-0.093-16.738-0.043-33.477-0.043-50.216
c0-0.739,0-1.477,0-2.18c-6.279,0-12.26,0-18.332,0c0,0.582,0,1.025,0,1.467c0.002,18.272-0.011,36.544,0.02,54.816
c0.002,1.846,0.132,3.707,0.391,5.533c1.626,11.411,7.669,19.567,18.546,23.533c11.212,4.09,22.621,3.836,33.771-0.445
c7.851-3.018,13.345-8.613,16.315-16.541c1.521-4.052,2.06-8.275,2.056-12.592c-0.018-17.953-0.008-35.905-0.008-53.858
C636.532,115.892,636.532,115.269,636.532,114.554z"/>
<path fill="#FFFFFF" d="M490.405,129.793c7.813,0,15.523,0,23.479,0c0,23.979,0,47.771,0,71.649c6.172,0,12.101,0,18.425,0
c0-23.949,0-47.739,0-71.729c7.947,0,15.655,0,23.378,0c0-5.158,0-10.115,0-15.116c-21.82,0-43.531,0-65.282,0
C490.405,119.691,490.405,124.648,490.405,129.793z"/>
<path fill="#FFFFFF" d="M673.407,114.58c-6.199,0-12.176,0-18.193,0c0,29.021,0,57.92,0,86.857c17.832,0,35.528,0,53.335,0
c0-5.191,0-10.146,0-15.306c-11.72,0-23.322,0-35.142,0C673.407,162.143,673.407,138.362,673.407,114.58z"/>
<path fill="#FFFFFF" d="M738.668,114.593c-6.197,0-12.172,0-18.216,0c0,28.985,0,57.833,0,86.831c17.905,0,35.6,0,53.371,0
c0-5.106,0-10.107,0-15.316c-11.729,0-23.335,0-35.155,0C738.668,162.119,738.668,138.339,738.668,114.593z"/>
<path fill="#FFFFFF" d="M786.183,201.464c6.083,0,12.02,0,18.095,0c0-29.09,0-57.987,0-86.879c-6.12,0-12.097,0-18.095,0
C786.183,143.612,786.183,172.513,786.183,201.464z"/>
<ellipse fill="#E5A00D" cx="795.229" cy="91.416" rx="12.458" ry="12.44"/>
<image overflow="visible" opacity="0.2" width="279" height="216" xlink:href="13AD46AC.png" transform="matrix(1 0 0 1 15.5 73.5)">
</image>
<g>
<path fill="#FFFFFF" d="M243.77,212.601c-10.482,0-19.533,6.118-23.783,14.977l-37.755-5.478
c-0.133-14.445-11.881-26.114-26.356-26.114c-5.163,0-9.975,1.489-14.041,4.051l-53.649-60.787
c3.091-4.319,4.914-9.605,4.914-15.319c0-14.56-11.802-26.362-26.363-26.362c-14.56,0-26.363,11.803-26.363,26.362
c0,14.559,11.803,26.361,26.363,26.361c4.484,0,8.704-1.122,12.399-3.096l54.223,61.438c-2.439,3.994-3.846,8.688-3.846,13.712
c0,14.56,11.803,26.363,26.364,26.363c10.422,0,19.43-6.05,23.708-14.828l37.832,5.491c0.22,14.369,11.929,25.953,26.354,25.953
c14.56,0,26.363-11.804,26.363-26.362S258.33,212.601,243.77,212.601z"/>
</g>
</g>
<g>
<image overflow="visible" opacity="0.2" width="279" height="279" xlink:href="13AD46AD.png" transform="matrix(1 0 0 1 15.5 10.5)">
</image>
<g>
<path fill="#E5A00D" d="M243.77,34.779c-14.56,0-26.363,11.802-26.363,26.362c0,7.17,2.867,13.667,7.512,18.42l-58.424,82.548
c-3.25-1.431-6.84-2.23-10.619-2.23c-14.56,0-26.363,11.8-26.363,26.36c0,2.592,0.38,5.094,1.078,7.46L86.2,221.181
c-4.82-5.271-11.754-8.58-19.461-8.58c-14.56,0-26.363,11.803-26.363,26.361s11.803,26.362,26.363,26.362
c14.561,0,26.363-11.804,26.363-26.362c0-2.518-0.36-4.951-1.019-7.256l44.477-27.534c4.815,5.183,11.686,8.429,19.318,8.429
c14.561,0,26.364-11.803,26.364-26.361c0-6.424-2.301-12.31-6.119-16.883l58.973-83.322c2.72,0.948,5.637,1.468,8.677,1.468
c14.561,0,26.363-11.802,26.363-26.361C270.135,46.582,258.33,34.779,243.77,34.779z"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -3,20 +3,30 @@
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="298.961px" height="300px" viewBox="0 0 298.961 300" enable-background="new 0 0 298.961 300" xml:space="preserve">
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="193" xlink:href="93124B54.png" transform="matrix(1 0 0 1 22.6465 89.25)">
</image>
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.376,6.382-24.809,15.623l-39.383-5.715
c-0.139-15.068-12.392-27.242-27.493-27.242c-5.385,0-10.405,1.554-14.646,4.229l-55.963-63.414
c3.224-4.505,5.127-10.02,5.127-15.981c0-15.188-12.312-27.5-27.5-27.5s-27.5,12.312-27.5,27.5s12.312,27.5,27.5,27.5
c4.677,0,9.079-1.171,12.934-3.23l56.56,64.089c-2.544,4.168-4.012,9.065-4.012,14.307c0,15.188,12.312,27.5,27.5,27.5
c10.872,0,20.269-6.311,24.731-15.467l39.464,5.727c0.229,14.99,12.443,27.074,27.489,27.074c15.188,0,27.5-12.313,27.5-27.5
S256.998,215.25,241.811,215.25z"/>
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="259" xlink:href="93124B55.png" transform="matrix(1 0 0 1 22.6465 23.25)">
</image>
<path fill="#E5A00D" d="M241.811,29.75c-15.188,0-27.5,12.312-27.5,27.5c0,7.48,2.99,14.258,7.835,19.216l-60.943,86.113
c-3.389-1.493-7.134-2.329-11.076-2.329c-15.188,0-27.5,12.313-27.5,27.5c0,2.704,0.397,5.314,1.125,7.783l-46.306,28.668
c-5.028-5.5-12.261-8.951-20.3-8.951c-15.188,0-27.5,12.313-27.5,27.5s12.312,27.5,27.5,27.5s27.5-12.313,27.5-27.5
c0-2.627-0.376-5.165-1.064-7.571l46.396-28.723c5.022,5.407,12.189,8.794,20.15,8.794c15.188,0,27.5-12.313,27.5-27.5
c0-6.701-2.399-12.84-6.382-17.611l61.515-86.92c2.836,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,27.5-27.5
S256.998,29.75,241.811,29.75z"/>
<g>
<image overflow="visible" opacity="0.2" width="289" height="224" xlink:href="93124B54.png" transform="matrix(1 0 0 1 4.9805 70.5)">
</image>
<g>
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.375,6.382-24.809,15.623l-39.383-5.715
c-0.139-15.068-12.393-27.242-27.493-27.242c-5.385,0-10.405,1.554-14.646,4.229l-55.963-63.414
c3.224-4.505,5.127-10.02,5.127-15.981c0-15.188-12.312-27.5-27.5-27.5s-27.5,12.312-27.5,27.5s12.312,27.5,27.5,27.5
c4.677,0,9.079-1.171,12.934-3.23l56.56,64.089c-2.544,4.168-4.012,9.064-4.012,14.307c0,15.188,12.312,27.5,27.5,27.5
c10.872,0,20.269-6.311,24.731-15.467l39.463,5.727c0.229,14.99,12.443,27.074,27.49,27.074c15.188,0,27.5-12.313,27.5-27.5
S256.998,215.25,241.811,215.25z"/>
</g>
</g>
<g>
<image overflow="visible" opacity="0.2" width="289" height="289" xlink:href="93124B55.png" transform="matrix(1 0 0 1 4.9805 5.5)">
</image>
<g>
<path fill="#E5A00D" d="M241.811,29.75c-15.188,0-27.5,12.312-27.5,27.5c0,7.48,2.99,14.258,7.836,19.216l-60.943,86.113
c-3.389-1.493-7.135-2.329-11.076-2.329c-15.188,0-27.5,12.313-27.5,27.5c0,2.704,0.397,5.314,1.125,7.783l-46.306,28.668
c-5.028-5.5-12.261-8.951-20.3-8.951c-15.188,0-27.5,12.313-27.5,27.5s12.312,27.5,27.5,27.5s27.5-12.313,27.5-27.5
c0-2.627-0.376-5.165-1.064-7.57l46.396-28.724c5.022,5.407,12.189,8.794,20.15,8.794c15.188,0,27.5-12.313,27.5-27.5
c0-6.701-2.399-12.84-6.382-17.611l61.515-86.92c2.837,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,27.5-27.5
S256.998,29.75,241.811,29.75z"/>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -2,43 +2,43 @@
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="953.503px" height="211.614px" viewBox="0 0 953.503 211.614" enable-background="new 0 0 953.503 211.614"
width="956.251px" height="214.137px" viewBox="0 -12.083 956.251 214.137" enable-background="new 0 -12.083 956.251 214.137"
xml:space="preserve">
<g>
<path fill="none" d="M-7.087,418.282c12.933,0,25.607,0,38.673,0c-6.5-21.354-12.936-42.492-19.56-64.253
C5.548,375.804-0.747,396.967-7.087,418.282z"/>
<path fill="#FFFFFF" d="M54.064,394.482c-7.594-21.64-15.215-43.269-22.824-64.903c-13.035,0-26.072,0-39.106,0.002
c-0.241,1.06-0.381,2.153-0.737,3.174c-14.675,42.06-29.37,84.111-44.058,126.164c-2.503,7.166-4.984,14.34-7.53,21.666
<path fill="none" d="M165.163,137.282c12.933,0,25.607,0,38.673,0c-6.5-21.354-12.936-42.492-19.56-64.253
C177.798,94.804,171.503,115.967,165.163,137.282z"/>
<path fill="#FFFFFF" d="M226.314,113.482c-7.594-21.64-15.215-43.269-22.824-64.903c-13.035,0-26.072,0-39.106,0.002
c-0.241,1.061-0.381,2.153-0.737,3.174c-14.675,42.061-29.37,84.111-44.058,126.164c-2.503,7.166-4.984,14.34-7.53,21.666
c11.54,0,22.78,0,34.162,0c3.614-11.881,7.2-23.67,10.784-35.455c18.283,0,36.34,0,54.529,0
c3.627,11.914,7.212,23.691,10.781,35.412c11.493,0,22.693,0,34.128,0c-0.397-1.166-0.705-2.094-1.029-3.02
C73.466,449.84,63.776,422.157,54.064,394.482z M-7.087,418.282c6.34-21.314,12.635-42.478,19.113-64.253
c6.624,21.761,13.06,42.899,19.56,64.253C18.52,418.282,5.846,418.282-7.087,418.282z"/>
<path fill="#FFFFFF" d="M-59.334,330.243c-37.826,0-75.371,0-112.916,0c0,8.836,0,17.674,0.001,26.514c13.525,0,27.05,0,40.889,0
c0,41.561,0,82.594,0,123.787c10.64,0,20.961,0,31.692,0c0-41.311,0-82.439,0-123.892c13.718,0,27.028,0,40.334,0
C-59.334,347.68-59.334,339.04-59.334,330.243z"/>
<path fill="#FFFFFF" d="M219.484,330.409c-10.657,0-20.976,0-31.667,0c0,1.457,0,2.752,0,4.045c0,28.379,0.037,56.758-0.037,85.135
C245.716,168.84,236.026,141.157,226.314,113.482z M165.163,137.282c6.34-21.314,12.635-42.478,19.113-64.253
c6.624,21.761,13.06,42.899,19.56,64.253C190.77,137.282,178.096,137.282,165.163,137.282z"/>
<path fill="#FFFFFF" d="M112.916,49.243c-37.826,0-75.371,0-112.916,0c0,8.836,0,17.674,0.001,26.514c13.525,0,27.05,0,40.889,0
c0,41.563,0,82.594,0,123.787c10.64,0,20.961,0,31.692,0c0-41.311,0-82.438,0-123.892c13.718,0,27.028,0,40.334,0
C112.916,66.68,112.916,58.04,112.916,49.243z"/>
<path fill="#FFFFFF" d="M391.734,49.409c-10.657,0-20.976,0-31.667,0c0,1.457,0,2.752,0,4.045c0,28.379,0.037,56.758-0.037,85.135
c-0.012,4.623-0.206,9.287-0.851,13.857c-1.723,12.219-7.998,19.277-19.089,21.752c-6.441,1.436-12.932,1.305-19.396,0.002
c-9.917-2-16.129-8.066-18.262-17.797c-1.171-5.336-1.655-10.92-1.685-16.398c-0.158-28.928-0.074-57.86-0.074-86.791
c0-1.268,0-2.535,0-3.717c-10.869,0-21.188,0-31.639,0c0,1.023,0,1.793,0,2.562c0,31.47-0.018,62.94,0.032,94.411
c0.005,3.299,0.217,6.629,0.683,9.896c2.816,19.719,13.26,33.813,32.036,40.645c19.239,7.002,38.813,6.619,57.978-0.678
c-9.917-2-16.129-8.066-18.262-17.797c-1.171-5.336-1.655-10.92-1.685-16.398c-0.158-28.928-0.074-57.859-0.074-86.791
c0-1.268,0-2.534,0-3.717c-10.869,0-21.188,0-31.639,0c0,1.023,0,1.793,0,2.563c0,31.471-0.018,62.939,0.032,94.41
c0.005,3.301,0.217,6.629,0.683,9.896c2.816,19.719,13.26,33.813,32.036,40.645c19.239,7.002,38.813,6.619,57.978-0.678
c13.699-5.215,23.273-14.91,28.45-28.723c2.545-6.791,3.528-13.869,3.526-21.104c-0.011-31.363-0.005-62.723-0.005-94.082
C219.484,332.463,219.484,331.502,219.484,330.409z"/>
<path fill="#FFFFFF" d="M488.595,333.793c0-1.073,0-2.149,0-3.385c-10.631,0-20.952,0-31.668,0c0,1.598,0,2.803,0,4.008
c0,28.488,0.036,56.979-0.037,85.467c-0.011,4.51-0.215,9.064-0.844,13.525c-1.734,12.314-8.183,19.514-19.379,21.869
c-6.339,1.332-12.722,1.215-19.076-0.064c-9.912-1.996-16.146-8.039-18.291-17.77c-1.174-5.334-1.662-10.92-1.69-16.398
c-0.159-28.928-0.075-57.86-0.075-86.791c0-1.277,0-2.553,0-3.769c-10.835,0-21.158,0-31.641,0c0,1.009,0,1.772,0,2.537
c0.003,31.581-0.016,63.161,0.034,94.743c0.005,3.189,0.229,6.406,0.676,9.563c2.806,19.723,13.238,33.82,32.007,40.674
c19.354,7.068,39.046,6.631,58.289-0.77c13.549-5.215,23.032-14.887,28.161-28.588c2.623-7.006,3.555-14.305,3.549-21.764
C488.578,395.852,488.595,364.823,488.595,333.793z"/>
<path fill="#FFFFFF" d="M236.383,356.745c13.484,0,26.794,0,40.523,0c0,41.445,0,82.566,0,123.836c10.655,0,20.887,0,31.803,0
c0-41.391,0-82.51-0.001-123.974c13.718,0,27.021,0,40.351,0c0-8.916,0-17.481,0-26.125c-37.661,0-75.134,0-112.676,0
C236.383,339.288,236.383,347.853,236.383,356.745z"/>
<path fill="#FFFFFF" d="M552.24,330.454c-10.699,0-21.014,0-31.4,0c0,50.158,0,100.104,0,150.119c30.779,0,61.322,0,92.055,0
c0-8.973,0-17.535,0-26.453c-20.227,0-40.254,0-60.654,0C552.24,412.659,552.24,371.556,552.24,330.454z"/>
<path fill="#FFFFFF" d="M664.878,330.476c-10.697,0-21.01,0-31.441,0c0,50.094,0,99.954,0,150.073c30.904,0,61.445,0,92.119,0
c0-8.826,0-17.469,0-26.473c-20.246,0-40.273,0-60.678,0C664.878,412.618,664.878,371.515,664.878,330.476z"/>
<path fill="#FFFFFF" d="M746.886,480.62c10.502,0,20.748,0,31.232,0c0-50.277,0-100.226,0-150.158c-10.563,0-20.879,0-31.232,0
C746.886,380.631,746.886,430.583,746.886,480.62z"/>
<circle fill="#E5A00D" cx="762.501" cy="290.417" r="21.5"/>
C391.734,51.463,391.734,50.502,391.734,49.409z"/>
<path fill="#FFFFFF" d="M660.845,52.793c0-1.073,0-2.149,0-3.385c-10.631,0-20.953,0-31.668,0c0,1.598,0,2.803,0,4.008
c0,28.487,0.035,56.979-0.037,85.467c-0.012,4.51-0.215,9.064-0.844,13.525c-1.734,12.313-8.184,19.514-19.379,21.869
c-6.34,1.332-12.723,1.215-19.076-0.064c-9.912-1.996-16.146-8.039-18.291-17.77c-1.174-5.334-1.662-10.92-1.689-16.398
c-0.16-28.928-0.076-57.86-0.076-86.791c0-1.277,0-2.553,0-3.77c-10.834,0-21.158,0-31.641,0c0,1.01,0,1.771,0,2.537
c0.004,31.581-0.016,63.16,0.033,94.742c0.006,3.189,0.23,6.406,0.678,9.564c2.805,19.723,13.236,33.82,32.006,40.674
c19.354,7.068,39.047,6.631,58.289-0.77c13.549-5.217,23.031-14.889,28.16-28.59c2.623-7.006,3.557-14.305,3.551-21.764
C660.828,114.852,660.845,83.823,660.845,52.793z"/>
<path fill="#FFFFFF" d="M408.633,75.745c13.484,0,26.794,0,40.523,0c0,41.445,0,82.566,0,123.836c10.655,0,20.887,0,31.802,0
c0-41.391,0-82.51,0-123.974c13.717,0,27.02,0,40.35,0c0-8.916,0-17.481,0-26.125c-37.66,0-75.133,0-112.675,0
C408.633,58.288,408.633,66.853,408.633,75.745z"/>
<path fill="#FFFFFF" d="M724.49,49.454c-10.699,0-21.014,0-31.4,0c0,50.158,0,100.104,0,150.119c30.779,0,61.322,0,92.055,0
c0-8.973,0-17.535,0-26.453c-20.227,0-40.254,0-60.652,0C724.49,131.659,724.49,90.556,724.49,49.454z"/>
<path fill="#FFFFFF" d="M837.128,49.476c-10.697,0-21.01,0-31.441,0c0,50.094,0,99.954,0,150.073c30.904,0,61.445,0,92.119,0
c0-8.826,0-17.469,0-26.473c-20.246,0-40.273,0-60.678,0C837.128,131.618,837.128,90.515,837.128,49.476z"/>
<path fill="#FFFFFF" d="M919.136,199.62c10.502,0,20.748,0,31.232,0c0-50.277,0-100.226,0-150.158c-10.564,0-20.879,0-31.232,0
C919.136,99.631,919.136,149.583,919.136,199.62z"/>
<circle fill="#E5A00D" cx="934.751" cy="9.417" r="21.5"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -11,7 +11,15 @@
<div class="row">
<div class="col-md-12">
<div class="padded-header" id="current-activity-header">
<h3>Activity &nbsp;&nbsp;<small><span id="currentActivityHeader"></span></small></h3>
<h3><span id="sessions-shortcut">Activity</span> &nbsp;&nbsp;
<small>
<span id="currentActivityHeader" style="display: none;">
Streams: <span id="currentActivityHeader-streams"></span> |
Bandwidth: <span id="currentActivityHeader-bandwidth"></span>
<span id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
</span>
</small>
</h3>
</div>
<div id="currentActivity">
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
@@ -199,6 +207,7 @@
<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';
@@ -235,7 +244,14 @@
}
};
var create_instances = [];
var activity_ready = true;
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
function getCurrentActivity() {
activity_ready = false;
$.ajax({
url: 'get_activity',
type: 'GET',
@@ -261,7 +277,7 @@
% else:
var msg_settings = ''
% endif
$('#currentActivityHeader').text('');
$('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
return
}
@@ -275,20 +291,25 @@
// Update the header stream counts
var sc_dp = current_activity.stream_count_direct_play,
sc_ds = current_activity.stream_count_direct_stream,
sc_tc = current_activity.stream_count_transcode;
var header_count = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
sc_tc = current_activity.stream_count_transcode,
total_bw = current_activity.total_bandwidth;
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
if (sc_dp) {
header_count += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
}
if (sc_ds) {
header_count += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', ';
streams_header += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', ';
}
if (sc_tc) {
header_count += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', ';
streams_header += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', ';
}
header_count = header_count.replace(/, $/, '');
header_count += ')';
$('#currentActivityHeader').text(header_count);
streams_header = streams_header.replace(/, $/, '') + ')';
$('#currentActivityHeader-streams').text(streams_header);
var bandwidth_header = (total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps');
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
$('#currentActivityHeader').show();
sessions.forEach(function (session) {
var s = new Proxy(session, defaultHandler);
@@ -298,6 +319,7 @@
// Create a new instance if it doesn't exist
if (!(instance.length)) {
create_instances.push(key);
getActivityInstance(key);
return;
}
@@ -498,9 +520,11 @@
});
} else {
$('#currentActivityHeader').text('');
$('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>');
}
activity_ready = true;
}
});
}
@@ -520,13 +544,20 @@
$('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 });
$('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller');
var index = create_instances.indexOf(session_key);
if (index > -1) {
create_instances.splice(index, 1);
}
}
});
}
getCurrentActivity();
setInterval(function () {
getCurrentActivity();
if (!(create_instances.length) && activity_ready) {
getCurrentActivity();
}
}, 2000);
setInterval(function(){
@@ -617,6 +648,12 @@
});
});
});
$('#sessions-shortcut').on('tripleclick', function () {
$.getJSON('return_sessions_url', function(sessions_url) {
window.open(sessions_url, '_blank');
});
});
% endif
</script>
% endif

View File

@@ -80,7 +80,7 @@ DOCUMENTATION :: END
<div class="col-md-12">
<div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb">
% if data['media_type'] == 'movie':
% 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>
% elif data['media_type'] == 'show':
@@ -116,9 +116,9 @@ DOCUMENTATION :: END
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track':
<a href="${config['pms_web_url'] or 'https://app.plex.tv/desktop'}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
% else:
<a href="${config['pms_web_url'] or 'https://app.plex.tv/desktop'}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
% endif
% if data['media_type'] == 'episode':
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
@@ -151,7 +151,7 @@ DOCUMENTATION :: END
</a>
</div>
<div class="summary-content-title">
% if data['media_type'] in ('movie', 'show', 'artist'):
% if data['media_type'] in ('movie', 'show', 'artist', 'collection'):
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['media_type'] == 'season':
<h1>&nbsp;</h1><h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
@@ -175,7 +175,7 @@ DOCUMENTATION :: END
<div class="col-md-9">
% if data['media_type'] == 'movie':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
% elif data['media_type'] == 'show' or data['media_type'] == 'season':
% elif data['media_type'] in ('show', 'season', 'collection'):
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
% elif data['media_type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;">
@@ -235,6 +235,8 @@ DOCUMENTATION :: END
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
Released <strong> ${data['year']}</strong>
% elif data['media_type'] == 'collection':
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
% endif
</div>
<div class="summary-content-details-tag">
@@ -308,51 +310,65 @@ DOCUMENTATION :: END
</div>
% if data['media_type'] == 'show':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Season List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
</div>
</div>
% elif data['media_type'] == 'season':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Episode List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
</div>
</div>
% elif data['media_type'] == 'artist':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Album List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
</div>
</div>
% elif data['media_type'] == 'album':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Track List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div>
</div>
</div>
% endif
% elif data['media_type'] == 'collection':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Movies in <strong>${data['title']}</strong> collection</span>
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading movies list...</div>
</div>
</div>
<div id="collection-related-list-container" style="display: none;">
</div>
% endif
% if data['media_type'] != 'collection':
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span>Watch History for <strong>${data['title']}</strong></span>
</div>
@@ -420,6 +436,7 @@ DOCUMENTATION :: END
</table>
</div>
</div>
% endif
</div>
</div>
</div>
@@ -562,6 +579,7 @@ DOCUMENTATION :: END
}
</script>
% endif
% if data['media_type'] != 'collection':
<script>
$(document).ready(function () {
get_history();
@@ -638,7 +656,8 @@ DOCUMENTATION :: END
});
});
</script>
% if data['media_type'] in ('show', 'season', 'artist', 'album'):
% endif
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
<script>
$.ajax({
url: 'get_item_children',
@@ -646,7 +665,24 @@ DOCUMENTATION :: END
async: true,
data: { rating_key : "${data['rating_key']}" },
complete: function(xhr, status) {
$("#children-list").html(xhr.responseText); }
$("#children-list").html(xhr.responseText);
}
});
</script>
% endif
% if data['media_type'] == 'collection':
<script>
$.ajax({
url: 'get_item_children_related',
type: 'GET',
async: true,
data: {
rating_key : "${data['rating_key']}",
title: "${data['title']}"
},
complete: function(xhr, status) {
$("#collection-related-list-container").html(xhr.responseText).show();
}
});
</script>
% endif

View File

@@ -37,13 +37,42 @@ DOCUMENTATION :: END
% else:
<li>
% endif
%if data['children_type'] == 'season':
% if data['children_type'] == 'movie':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
<div class="item-children-instance-text-wrapper poster-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
% elif data['children_type'] == 'show':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
<div class="item-children-instance-text-wrapper poster-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
</div>
% elif data['children_type'] == 'season':
<a href="info?rating_key=${child['rating_key']}" title="Season ${child['media_index']}">
<div class="item-children-poster">
% if child['thumb']:
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
% else:
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
% endif
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
@@ -59,7 +88,7 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'episode':
<a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}">
<div class="item-children-poster">
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
Episode ${child['media_index']}
@@ -79,13 +108,13 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'album':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
<div class="item-children-instance-text-wrapper album-item">
<div class="item-children-instance-text-wrapper cover-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>

View File

@@ -0,0 +1,99 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: info_collection_list.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
children_type Returns the type of children in the array.
children_count Returns the number of episodes in the array.
children_list Returns an array of episodes.
data['children_list'] :: Usable paramaters
== Global keys ==
rating_key Returns the unique identifier for the media item.
media_index Returns the episode number.
title Returns the name of the episode.
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
parent_thumb Returns the location of the item's parent thumbnail. Use with pms_image_proxy.
DOCUMENTATION :: END
</%doc>
% if data != None:
<%
types = ('movie', 'show', 'artist', 'album')
headers = {'movie': 'Movies',
'show': 'TV Shows',
'season': 'Seasons',
'episode': 'Episodes',
'artist': 'Artists',
'album': 'Albums',
'track': 'Tracks',
}
%>
% for media_type in types:
% if data['results_list'][media_type]:
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span>${headers[media_type]} in <strong>${title}</strong> collection</span>
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list">
<div class="item-children-wrapper">
<ul class="item-children-instance list-unstyled">
% for child in data['results_list'][media_type]:
<li>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
% if media_type in ('artist', 'album'):
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% else:
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% endif
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
% if media_type == 'artist':
<div class="item-children-instance-text-wrapper cover-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
</div>
% elif media_type == 'album':
<div class="item-children-instance-text-wrapper cover-item">
<h3>
<a href="info?rating_key=${child['parent_rating_key']}" title="${child['parent_title']}">${child['parent_title']}</a>
</h3>
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
</div>
% else:
<div class="item-children-instance-text-wrapper poster-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
% endif
</li>
% endfor
</ul>
</div>
</div>
</div>
</div>
% endif
% endfor
% endif

View File

@@ -54,6 +54,31 @@ DOCUMENTATION :: END
% if data != None:
% if data['results_count'] > 0:
% if 'collection' in data['results_list'] and data['results_list']['collection']:
<div class="item-children-wrapper">
<div class="item-children-section-title">
<h4>Collections</h4>
</div>
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['collection']:
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['min_year']} - ${child['max_year']}</h3>
</div>
</a>
</li>
% endfor
</ul>
</div>
% endif
% if 'movie' in data['results_list'] and data['results_list']['movie']:
<div class="item-children-wrapper">
<div class="item-children-section-title">
@@ -64,12 +89,12 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper season-item">
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
@@ -89,12 +114,12 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper season-item">
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
@@ -114,12 +139,12 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper season-item">
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
<h3 class="text-muted">S${child['media_index']}</h3>
</div>
@@ -139,7 +164,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -165,7 +190,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="item-children-poster-face cover-item style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -189,7 +214,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -214,7 +239,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
Track ${child['media_index']}

View File

@@ -0,0 +1 @@
!function(t){function i(t){var i=jQuery(this);settings=jQuery.extend({},e,t.data);var a=i.data("triclick_clicks")||0,c=i.data("triclick_start")||0;0===a&&(c=t.timeStamp),0!=c&&t.timeStamp>c+settings.threshold&&(a=0,c=t.timeStamp),a+=1,3===a&&(a=0,c=0,t.type="tripleclick",void 0===jQuery.event.handle?jQuery.event.dispatch.apply(this,arguments):jQuery.event.handle.apply(this,arguments)),i.data("triclick_clicks",a),i.data("triclick_start",c)}var e={threshold:1e3};t.event.special.tripleclick={setup:function(e,a){t(this).bind("touchstart click.triple",e,i)},teardown:function(e){t(this).unbind("touchstart click.triple",i)}}}(jQuery);

View File

@@ -59,6 +59,8 @@ history_table_options = {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-pause fa-fw"></i></span>';
} else if (rowData['state'] === 'buffering') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-spinner fa-fw"></i></span>';
} else if (rowData['state'] === 'stopped') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
}
$(td).html('<div><div style="float: left;">' + state + '&nbsp;' + date + '</div></div>');
} else if (rowData['group_count'] > 1) {
@@ -203,7 +205,7 @@ history_table_options = {
"targets": [9],
"data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null || rowData['state'] != null) {
if (cellData === null || rowData['state'] != "stopped") {
$(td).html('n/a');
} else {
$(td).html(moment(cellData,"X").format(time_format));

View File

@@ -47,7 +47,7 @@ sync_table_options = {
},
{
"targets": [2],
"data": "friendly_name",
"data": "user",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id']) {
@@ -63,7 +63,7 @@ sync_table_options = {
},
{
"targets": [3],
"data": "title",
"data": "sync_title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['metadata_type'] !== '') {

View File

@@ -71,11 +71,11 @@ DOCUMENTATION :: END
% endif
</div>
<div class="user-info-nav">
<ul class="user-info-nav">
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#libraryHistory" data-toggle="tab">History</a></li>
<ul class="user-info-nav" role="tablist">
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
% if _session['user_group'] == 'admin':
<li><a id="media-info-tab-btn" href="#libraryMediaInfo" data-toggle="tab">Media Info</a></li>
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
% endif
</ul>
</div>
@@ -83,7 +83,7 @@ DOCUMENTATION :: END
</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="profile">
<div role="tabpanel" class="tab-pane active" id="tabs-profile">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -169,7 +169,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="libraryHistory">
<div role="tabpanel" class="tab-pane" id="tabs-history">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -221,7 +221,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="libraryMediaInfo">
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -366,8 +366,253 @@ DOCUMENTATION :: END
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
<script>
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
function loadHistoryTable() {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
};
}
}
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
}
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
loadHistoryTable();
});
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
media_info_table_options.ajax = {
url: 'get_library_media_info',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
refresh: refresh_table
};
}
}
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-media-info');
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
}
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
loadMediaInfoTable();
});
$("#refresh-media-info-table").click(function () {
media_info_child_table = {};
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
refresh_table = false;
});
$("#edit-library-tooltip").tooltip();
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
% endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'library_recently_watched',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-watched").html(xhr.responseText);
highlightWatchedScrollerButton();
}
});
}
function recentlyAdded() {
// Populate recently added
$.ajax({
url: 'library_recently_added',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-added").html(xhr.responseText);
highlightAddedScrollerButton();
}
});
}
recentlyWatched();
recentlyAdded();
function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-watched").width()) {
$("#recently-watched-page-right").removeClass("disabled");
} else {
$("#recently-watched-page-right").addClass("disabled");
}
}
function highlightAddedScrollerButton() {
var scroller = $("#recently-added-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-added").width()) {
$("#recently-added-page-right").removeClass("disabled");
} else {
$("#recently-added-page-right").addClass("disabled");
}
}
$(window).resize(function() {
highlightWatchedScrollerButton();
highlightAddedScrollerButton();
});
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
var leftTotalWatched = 0;
$(".paginate-watched").click(function (e) {
e.preventDefault();
var scroller = $("#recently-watched-row-scroller");
var containerWidth = $("#library-recently-watched").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalWatched }, 250);
if (leftTotalWatched == 0) {
$("#recently-watched-page-left").addClass("disabled").blur();
} else {
$("#recently-watched-page-left").removeClass("disabled");
}
if (leftTotalWatched == leftMax) {
$("#recently-watched-page-right").addClass("disabled").blur();
} else {
$("#recently-watched-page-right").removeClass("disabled");
}
});
var leftTotalAdded = 0;
$(".paginate-added").click(function (e) {
e.preventDefault();
var scroller = $("#recently-added-row-scroller");
var containerWidth = $("#library-recently-added").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalAdded }, 250);
if (leftTotalAdded == 0) {
$("#recently-added-page-left").addClass("disabled").blur();
} else {
$("#recently-added-page-left").removeClass("disabled");
}
if (leftTotalAdded == leftMax) {
$("#recently-added-page-right").addClass("disabled").blur();
} else {
$("#recently-added-page-right").removeClass("disabled");
}
});
$(document).ready(function () {
$("#edit-library-tooltip").tooltip();
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
}
// Change hash for page-reload
$('.user-info-nav a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
// Populate watch time stats
$.ajax({
@@ -389,237 +634,6 @@ DOCUMENTATION :: END
}
});
function loadHistoryTable() {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
};
}
}
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
}
$( "#history-tab-btn" ).one( "click", function() {
loadHistoryTable();
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
media_info_table_options.ajax = {
url: 'get_library_media_info',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
refresh: refresh_table
};
}
}
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-media-info');
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
}
$( "#media-info-tab-btn" ).one( "click", function() {
loadMediaInfoTable();
});
$("#refresh-media-info-table").click(function () {
media_info_child_table = {};
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
refresh_table = false;
});
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
% endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'library_recently_watched',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-watched").html(xhr.responseText);
highlightWatchedScrollerButton();
}
});
}
function recentlyAdded() {
// Populate recently added
$.ajax({
url: 'library_recently_added',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-added").html(xhr.responseText);
highlightAddedScrollerButton();
}
});
}
recentlyWatched();
recentlyAdded();
function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-watched").width()) {
$("#recently-watched-page-right").removeClass("disabled");
} else {
$("#recently-watched-page-right").addClass("disabled");
}
}
function highlightAddedScrollerButton() {
var scroller = $("#recently-added-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-added").width()) {
$("#recently-added-page-right").removeClass("disabled");
} else {
$("#recently-added-page-right").addClass("disabled");
}
}
$(window).resize(function() {
highlightWatchedScrollerButton();
highlightAddedScrollerButton();
});
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
var leftTotalWatched = 0;
$(".paginate-watched").click(function (e) {
e.preventDefault();
var scroller = $("#recently-watched-row-scroller");
var containerWidth = $("#library-recently-watched").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalWatched }, 250);
if (leftTotalWatched == 0) {
$("#recently-watched-page-left").addClass("disabled").blur();
} else {
$("#recently-watched-page-left").removeClass("disabled");
}
if (leftTotalWatched == leftMax) {
$("#recently-watched-page-right").addClass("disabled").blur();
} else {
$("#recently-watched-page-right").removeClass("disabled");
}
});
var leftTotalAdded = 0;
$(".paginate-added").click(function (e) {
e.preventDefault();
var scroller = $("#recently-added-row-scroller");
var containerWidth = $("#library-recently-added").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalAdded }, 250);
if (leftTotalAdded == 0) {
$("#recently-added-page-left").addClass("disabled").blur();
} else {
$("#recently-added-page-left").removeClass("disabled");
}
if (leftTotalAdded == leftMax) {
$("#recently-added-page-right").addClass("disabled").blur();
} else {
$("#recently-added-page-right").removeClass("disabled");
}
});
});
</script>
% endif

View File

@@ -3,7 +3,7 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tautulli - ${title} | ${server_name}</title>
<title>Tautulli - ${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
@@ -14,28 +14,21 @@
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.0">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS -->
<!-- Android >M39 icon -->
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0">
<meta name="theme-color" content="#1f1f1f">
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.0" color="#1f1f1f">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="initial-scale=1">
<meta name="format-detection" content="telephone=no">
<!-- IE10 icon -->
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-TileColor" content="#1f1f1f">
<meta name="msapplication-TileImage" content="${http_root}images/favicon/mstile-144x144.png?v=2.0.0">
<meta name="msapplication-config" content="${http_root}xml/IEconfig.xml?v=2.0.0" />
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head>
<body>
@@ -44,7 +37,7 @@
<div class="row">
<div class="login-container">
<div class="login-logo">
<img alt="Tautulli" src="${http_root}images/logo-tautulli.svg" height="100">
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 100px;"></object>
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">

View File

@@ -45,7 +45,7 @@ DOCUMENTATION :: END
% elif job in ('Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
<tr>
<td>${job}</td>
<td><i class="fa fa-sm fa-fw fa-check"></i> Using Websocket</td>
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
<td>N/A</td>
<td>N/A</td>
<td>N/A</td>

View File

@@ -18,7 +18,7 @@
</div>
</div>
<div class='table-card-back'>
<div id="search-results-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading search results...</div>
<div id="search-results-list" class="search-results-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading search results...</div>
</div>
</div>
</%def>

View File

@@ -559,7 +559,7 @@
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div>
<div class="form-group has-feedback" id="pms-ip-group">
<div class="form-group has-feedback" id="pms_ip_group">
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
<div class="col-md-6">
@@ -569,7 +569,7 @@
<button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button>
</span>
</div>
<span class="form-control-feedback" id="pms-verify" aria-hidden="true" style="display: none; right: 110px;"></span>
<span class="form-control-feedback" id="pms_verify" aria-hidden="true" style="display: none; right: 110px;"></span>
</div>
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
@@ -607,7 +607,12 @@
<label for="pms_logs_folder">Plex Web URL</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/.+\/web\/index\.html$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL." required>
<div class="input-group">
<input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL.">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="test_pms_web_button">Test URL</button>
</span>
</div>
</div>
<div id="pms_web_url_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
@@ -648,7 +653,7 @@
</div>
<div class="form-group">
<label for="pms_token">PMS Token</label>
<label for="pms_token">Plex.tv Account Token</label>
<div class="row">
<div class="col-md-6">
<div class="input-group">
@@ -1110,7 +1115,8 @@
</div>
<div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
<strong>Please read the <a href="#" target="_blank" id="guidelines-link">guidelines</a> in the README document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
<strong>Please read the <a href="${anon_url('https://github.com/%s/plexpy/blob/master/CONTRIBUTING.md' % plexpy.CONFIG.GIT_USER)}" target="_blank">guidelines</a>
in the CONTRIBUTING document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
<br /><br />
Your post may be removed for failure to follow the guidelines.
</div>
@@ -1131,7 +1137,8 @@
</div>
<div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
<strong>Please read the <a href="#" target="_blank" id="faq-link">FAQ</a> before asking for help!</strong>
<strong>Please read the <a href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank">FAQ</a>
before asking for help!</strong>
</div>
</div>
<div class="modal-footer">
@@ -1580,6 +1587,17 @@ $(document).ready(function() {
}
}
function preSaveChecks(_callback) {
if ($("#pms_identifier").val() == "") {
verifyServer();
}
verifyPMSWebURL();
if (_callback) {
_callback();
}
}
// Alert the user that their changes require a restart.
function postSaveChecks() {
if (serverChanged || authChanged || httpChanged || directoryChanged) {
@@ -1609,11 +1627,7 @@ $(document).ready(function() {
}
$('.save-button').click(function() {
if ($("#pms_identifier").val() == "") {
verifyServer(function () { saveSettings() });
} else {
saveSettings();
}
preSaveChecks(function () { saveSettings() });
});
initConfigCheckbox('#api_enabled');
@@ -1758,43 +1772,45 @@ $(document).ready(function() {
var pms_identifier = $("#pms_identifier").val();
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
$.ajax({
url: 'get_server_id',
data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
data: {
hostname: pms_ip,
port: pms_port,
identifier: pms_identifier,
ssl: pms_ssl,
remote: pms_is_remote
},
cache: true,
async: true,
timeout: 10000,
error: function(jqXHR, textStatus, errorThrown) {
$("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error");
error: function (jqXHR, textStatus, errorThrown) {
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$("#pms_ip_group").addClass("has-error");
},
success: function (json) {
var machine_identifier = json;
if (machine_identifier) {
$("#pms_identifier").val(machine_identifier);
$("#pms-verify").html('<i class="fa fa-check"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").removeClass("has-error");
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
$("#pms_ip_group").removeClass("has-error");
if (_callback) {
_callback();
}
} else {
$("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error");
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$("#pms_ip_group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true)
}
}
});
} else {
$("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error");
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$("#pms_ip_group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true)
}
}
@@ -1804,11 +1820,21 @@ $(document).ready(function() {
verifyServer();
});
function verifyPMSWebURL() {
var pms_web_url = $.trim($("#pms_web_url").val());
$("#pms_web_url").val(pms_web_url || 'https://app.plex.tv/desktop');
}
$('#test_pms_web_button').on('click', function(){
var pms_web_url = $.trim($("#pms_web_url").val());
window.open(pms_web_url, '_blank');
});
// Plex.tv auth token fetch
$("#get-pms-auth-token").click(function() {
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Fetching token...');
var pms_username = $("#pms_username").val().trim();
var pms_password = $("#pms_password").val().trim();
var pms_username = $.trim($("#pms_username").val());
var pms_password = $.trim($("#pms_password").val());
if ((pms_username !== '') && (pms_password !== '')) {
$.ajax({
type: 'GET',

View File

@@ -40,7 +40,7 @@
<th align="left" id="delete_row">Delete</th>
<th align="left" id="state">State</th>
<th align="left" id="user">User</th>
<th align="left" id="title">Title</th>
<th align="left" id="sync_title">Title</th>
<th align="left" id="type">Type</th>
<th align="left" id="platform">Platform</th>
<th align="left" id="device">Device</th>

View File

@@ -61,19 +61,19 @@ DOCUMENTATION :: END
% endif
</div>
<div class="user-info-nav">
<ul class="user-info-nav">
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#userHistory" data-toggle="tab">History</a></li>
<li><a id="sync-tab-btn" href="#userSyncItems" data-toggle="tab">Synced Items</a></li>
<li><a id="ip-tab-btn" href="#userAddresses" data-toggle="tab">IP Addresses</a></li>
<li><a id="login-tab-btn" href="#userLogins" data-toggle="tab">Tautulli Logins</a></li>
<ul class="user-info-nav" role="tablist">
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
<li><a id="sync-tab-btn" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
<li><a id="ip-tab-btn" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
<li><a id="login-tab-btn" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
</ul>
</div>
</div>
</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="profile">
<div role="tabpanel" class="tab-pane active" id="tabs-profile">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -134,7 +134,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="userHistory">
<div role="tabpanel" class="tab-pane" id="tabs-history">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -200,7 +200,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="userSyncItems">
<div role="tabpanel" class="tab-pane" id="tabs-synceditems">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -240,7 +240,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="userAddresses">
<div role="tabpanel" class="tab-pane" id="tabs-ipaddresses">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -271,7 +271,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="userLogins">
<div role="tabpanel" class="tab-pane" id="tabs-tautullilogins">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -369,23 +369,246 @@ DOCUMENTATION :: END
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
% if data:
<script>
% if str(data['user_id']).isdigit():
var user_id = ${data['user_id']};
% else:
var user_id = null;
% endif
var username = '${data['username'].replace("'", "\\'")}';
</script>
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script src="${http_root}js/tables/user_ips.js${cache_param}"></script>
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
<script src="${http_root}js/tables/login_logs.js${cache_param}"></script>
<script>
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
$('a[href="#tabs-profile"]').on('shown.bs.tab', function() {
var media_type = null;
loadHistoryTable(media_type);
});
function loadHistoryTable(media_type) {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
user_id: user_id,
media_type: media_type
};
}
}
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-UID-${data["user_id"]}', history_table);
$('#media_type-selection').on('change', function () {
$('#media_type-selection > label').removeClass('active');
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
$(selected_filter).closest('label').addClass('active');
media_type = $(selected_filter).val();
history_table.draw();
});
}
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
var media_type = null;
loadHistoryTable(media_type);
});
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
// Build user sync table
sync_table_options.ajax = {
url: 'get_sync',
data: function(d) {
d.user_id = user_id;
}
}
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
sync_table.column(1).visible(false);
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis_sync.button() ).appendTo('#button-bar-sync');
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
});
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
// Build user IP table
user_ip_table_options.ajax = {
url: 'get_user_ips',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
user_id: user_id
};
}
}
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
});
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
// Build user login table
login_log_table_options.ajax = {
url: 'get_user_logins',
data: function(d) {
d.user_id = user_id;
}
}
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
login_log_table.columns([1, 2]).visible(false);
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis_login.button() ).appendTo('#button-bar-login');
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
});
% if _session['user_group'] == 'admin':
$("#edit-user-tooltip").tooltip();
// Load edit user modal
$("#toggle-edit-user-modal").click(function() {
$("#edit-user-tooltip").tooltip('hide');
$.ajax({
url: 'edit_user_dialog',
data: { user_id: user_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-user-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
% endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: {
user_id: user_id,
limit: 50
},
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
highlightWatchedScrollerButton();
}
});
}
recentlyWatched();
function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#user-recently-watched").width()) {
$("#recently-watched-page-right").removeClass("disabled");
} else {
$("#recently-watched-page-right").addClass("disabled");
}
}
$(window).resize(function() {
highlightWatchedScrollerButton();
});
var leftTotal = 0;
$(".paginate").click(function (e) {
e.preventDefault();
var scroller = $("#recently-watched-row-scroller");
var containerWidth = $("#user-recently-watched").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotal }, 250);
if (leftTotal == 0) {
$("#recently-watched-page-left").addClass("disabled").blur();
} else {
$("#recently-watched-page-left").removeClass("disabled");
}
if (leftTotal == leftMax) {
$("#recently-watched-page-right").addClass("disabled").blur();
} else {
$("#recently-watched-page-right").removeClass("disabled");
}
});
$(document).ready(function () {
% if str(data['user_id']).isdigit():
var user_id = ${data['user_id']};
% else:
var user_id = null;
% endif
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
}
var username = '${data['username'].replace("'", "\\'")}';
$("#edit-user-tooltip").tooltip();
// Change hash for page-reload
$('.user-info-nav a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
// Populate watch time stats
$.ajax({
@@ -407,210 +630,6 @@ DOCUMENTATION :: END
}
});
function loadHistoryTable(media_type) {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
user_id: user_id,
media_type: media_type
};
}
}
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-UID-${data["user_id"]}', history_table);
$('#media_type-selection').on('change', function () {
$('#media_type-selection > label').removeClass('active');
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
$(selected_filter).closest('label').addClass('active');
media_type = $(selected_filter).val();
history_table.draw();
});
}
$( "#history-tab-btn" ).one( "click", function() {
var media_type = null;
loadHistoryTable(media_type);
});
$( "#ip-tab-btn" ).one( "click", function() {
// Build user IP table
user_ip_table_options.ajax = {
url: 'get_user_ips',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
user_id: user_id
};
}
}
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
});
$( "#sync-tab-btn" ).one( "click", function() {
// Build user sync table
sync_table_options.ajax = {
url: 'get_sync',
data: function(d) {
d.user_id = user_id;
}
}
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
sync_table.column(1).visible(false);
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis_sync.button() ).appendTo('#button-bar-sync');
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
});
$( "#login-tab-btn" ).one( "click", function() {
// Build user login table
login_log_table_options.ajax = {
url: 'get_user_logins',
data: function(d) {
d.user_id = user_id;
}
}
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
login_log_table.columns([1, 2]).visible(false);
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis_login.button() ).appendTo('#button-bar-login');
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
% if _session['user_group'] == 'admin':
// Load edit user modal
$("#toggle-edit-user-modal").click(function() {
$("#edit-user-tooltip").tooltip('hide');
$.ajax({
url: 'edit_user_dialog',
data: { user_id: user_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-user-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
% endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: {
user_id: user_id,
limit: 50
},
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
highlightWatchedScrollerButton();
}
});
}
recentlyWatched();
function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#user-recently-watched").width()) {
$("#recently-watched-page-right").removeClass("disabled");
} else {
$("#recently-watched-page-right").addClass("disabled");
}
}
$(window).resize(function() {
highlightWatchedScrollerButton();
});
var leftTotal = 0;
$(".paginate").click(function (e) {
e.preventDefault();
var scroller = $("#recently-watched-row-scroller");
var containerWidth = $("#user-recently-watched").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotal }, 250);
if (leftTotal == 0) {
$("#recently-watched-page-left").addClass("disabled").blur();
} else {
$("#recently-watched-page-left").removeClass("disabled");
}
if (leftTotal == leftMax) {
$("#recently-watched-page-right").addClass("disabled").blur();
} else {
$("#recently-watched-page-right").removeClass("disabled");
}
});
});
</script>
% endif

View File

@@ -39,6 +39,10 @@ from cherrypy import wsgiserver
try:
from OpenSSL import SSL
from OpenSSL import crypto
if hasattr(SSL, 'Connection'):
SSLConnectionType = SSL.Connection
else:
SSLConnectionType = SSL.ConnectionType
except ImportError:
SSL = None
@@ -244,7 +248,7 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
return ssl_environ
def makefile(self, sock, mode='r', bufsize=-1):
if SSL and isinstance(sock, SSL.ConnectionType):
if SSL and isinstance(sock, SSLConnectionType):
timeout = sock.gettimeout()
f = SSL_fileobject(sock, mode, bufsize)
f.ssl_timeout = timeout

View File

@@ -36,12 +36,14 @@ import activity_handler
import activity_pinger
import config
import database
import libraries
import logger
import mobile_app
import notification_handler
import notifiers
import plextv
import pmsconnect
import users
import versioncheck
import plexpy.config
@@ -213,16 +215,15 @@ def initialize(config_file):
# Get the real PMS urls for SSL and remote access
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
plextv.get_real_pms_url()
pmsconnect.get_server_friendly_name()
plextv.get_server_resources()
# Refresh the users list on startup
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
plextv.refresh_users()
users.refresh_users()
# Refresh the libraries list on startup
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
pmsconnect.refresh_libraries()
libraries.refresh_libraries()
# Store the original umask
UMASK = os.umask(0)
@@ -323,14 +324,8 @@ def initialize_scheduler():
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
#schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
# hours=0, minutes=0, seconds=1)
#schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
# hours=0, minutes=0, seconds=monitor_seconds * bool(CONFIG.NOTIFY_RECENTLY_ADDED))
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0)
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name',
hours=12, minutes=0, seconds=0)
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
hours=0, minutes=0, seconds=60 * bool(CONFIG.MONITOR_REMOTE_ACCESS))
@@ -341,9 +336,9 @@ def initialize_scheduler():
user_hours = CONFIG.REFRESH_USERS_INTERVAL if 1 <= CONFIG.REFRESH_USERS_INTERVAL <= 24 else 12
library_hours = CONFIG.REFRESH_LIBRARIES_INTERVAL if 1 <= CONFIG.REFRESH_LIBRARIES_INTERVAL <= 24 else 12
schedule_job(plextv.refresh_users, 'Refresh users list',
schedule_job(users.refresh_users, 'Refresh users list',
hours=user_hours, minutes=0, seconds=0)
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
hours=library_hours, minutes=0, seconds=0)
schedule_job(activity_pinger.check_server_response, 'Check server response',
@@ -351,9 +346,7 @@ def initialize_scheduler():
else:
# Cancel all jobs
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
hours=0, minutes=0, seconds=0)
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name',
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
hours=0, minutes=0, seconds=0)
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
@@ -361,9 +354,9 @@ def initialize_scheduler():
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
hours=0, minutes=0, seconds=0)
schedule_job(plextv.refresh_users, 'Refresh users list',
schedule_job(users.refresh_users, 'Refresh users list',
hours=0, minutes=0, seconds=0)
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
hours=0, minutes=0, seconds=0)
# Schedule job to reconnect websocket

View File

@@ -278,7 +278,6 @@ def check_server_response():
def check_server_access():
with monitor_lock:
pms_connect = pmsconnect.PmsConnect()
server_response = pms_connect.get_server_response()
@@ -287,7 +286,7 @@ def check_server_access():
# Check for remote access
if server_response:
mapping_state = server_response['mapping_state']
mapping_error = server_response['mapping_error']

View File

@@ -360,9 +360,9 @@ class ActivityProcessor(object):
'subtitles': session['subtitles'],
'synced_version': session['synced_version'],
'synced_version_profile': session['synced_version_profile'],
'synced_version_title': session['synced_version_title'],
'optimized_version': session['optimized_version'],
'optimized_version_profile': session['optimized_version_profile']
'optimized_version_profile': session['optimized_version_profile'],
'optimized_version_title': session['optimized_version_title']
}
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_media_info transaction...")

View File

@@ -32,10 +32,10 @@ import xmltodict
import plexpy
import config
import database
import libraries
import logger
import mobile_app
import plextv
import pmsconnect
import users
class API2:
@@ -345,14 +345,14 @@ class API2:
def refresh_libraries_list(self, **kwargs):
""" Refresh the Tautulli libraries list."""
data = pmsconnect.refresh_libraries()
data = libraries.refresh_libraries()
self._api_result_type = 'success' if data else 'error'
return data
def refresh_users_list(self, **kwargs):
""" Refresh the Tautulli users list."""
data = plextv.refresh_users()
data = users.refresh_users()
self._api_result_type = 'success' if data else 'error'
return data

View File

@@ -140,10 +140,10 @@ SCHEDULER_LIST = ['Check GitHub for updates',
'Check for recently added items',
'Check for Plex updates',
'Check for Plex remote access',
'Check server response',
'Refresh users list',
'Refresh libraries list',
'Refresh Plex server URLs',
'Refresh Plex server name',
'Backup Tautulli database',
'Backup Tautulli config'
]

View File

@@ -45,6 +45,7 @@ _CONFIG_DEFINITIONS = {
'PLEXWATCH_DATABASE': (str, 'PlexWatch', ''),
'PMS_IDENTIFIER': (str, 'PMS', ''),
'PMS_IP': (str, 'PMS', '127.0.0.1'),
'PMS_IS_CLOUD': (int, 'PMS', 0),
'PMS_IS_REMOTE': (int, 'PMS', 0),
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000),

View File

@@ -1104,7 +1104,7 @@ class DataFactory(object):
if str(rating_key).isdigit():
poster_key = rating_key
elif metadata:
if metadata['media_type'] in ('movie', 'show', 'artist'):
if metadata['media_type'] in ('movie', 'show', 'artist', 'collection'):
poster_key = metadata['rating_key']
elif metadata['media_type'] in ('season', 'album'):
poster_key = metadata['rating_key']

View File

@@ -1,24 +1,28 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This file is part of Tautulli.
# This file is part of PlexPy.
#
# Tautulli is free software: you can redistribute it and/or modify
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Tautulli is distributed in the hope that it will be useful,
# PlexPy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from httplib import HTTPSConnection
from httplib import HTTPConnection
import ssl
from functools import partial
from multiprocessing.dummy import Pool as ThreadPool
from urlparse import urljoin
import certifi
from requests.packages import urllib3
from requests.packages.urllib3.exceptions import InsecureRequestWarning
import plexpy
import helpers
@@ -30,94 +34,144 @@ class HTTPHandler(object):
Retrieve data from Plex Server
"""
def __init__(self, host, port, token, ssl_verify=True):
self.host = host
self.port = str(port)
def __init__(self, urls, token=None, timeout=10, ssl_verify=True):
if isinstance(urls, basestring):
self.urls = urls.split() or urls.split(',')
else:
self.urls = urls
self.token = token
if self.token:
self.headers = {'X-Plex-Token': self.token}
else:
self.headers = {}
self.timeout = timeout
self.ssl_verify = ssl_verify
"""
Handle the HTTP requests.
self.valid_request_types = ('GET', 'POST', 'PUT', 'DELETE')
Output: object
"""
def make_request(self,
uri=None, proto='HTTP',
request_type='GET',
uri=None,
headers=None,
request_type='GET',
output_format='raw',
return_type=False,
no_token=False,
timeout=None):
timeout=None,
callback=None):
"""
Handle the HTTP requests.
if timeout is None:
timeout = plexpy.CONFIG.PMS_TIMEOUT
Output: list
"""
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
self.uri = uri
self.request_type = request_type.upper()
self.output_format = output_format.lower()
self.return_type = return_type
self.callback = callback
self.timeout = timeout or self.timeout
if request_type.upper() not in valid_request_types:
if self.request_type not in self.valid_request_types:
logger.debug(u"HTTP request made but unsupported request type given.")
return None
if uri:
if proto.upper() == 'HTTPS':
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
context = ssl._create_unverified_context()
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout, context=context)
logger.warn(u"Tautulli HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
else:
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout)
else:
handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout)
request_urls = [urljoin(url, self.uri) for url in self.urls]
if not no_token:
if headers:
headers.update({'X-Plex-Token': self.token})
else:
headers = {'X-Plex-Token': self.token}
if no_token and headers:
self.headers = headers
elif headers:
self.headers.update(headers)
try:
if headers:
handler.request(request_type, uri, headers=headers)
else:
handler.request(request_type, uri)
response = handler.getresponse()
request_status = response.status
request_content = response.read()
content_type = response.getheader('content-type')
except IOError as e:
logger.warn(u"Failed to access uri endpoint %s with error %s" % (uri, e))
return None
except Exception as e:
logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (uri, e))
return None
except:
logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % uri)
return None
responses = []
for r in self._http_requests_pool(request_urls):
responses.append(r)
if request_status in (200, 201):
try:
if output_format == 'dict':
output = helpers.convert_xml_to_dict(request_content)
elif output_format == 'json':
output = helpers.convert_xml_to_json(request_content)
elif output_format == 'xml':
output = helpers.parse_xml(request_content)
else:
output = request_content
return responses[0]
if return_type:
return output, content_type
return output
except Exception as e:
logger.warn(u"Failed format response from uri %s to %s error %s" % (uri, output_format, e))
return None
else:
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status))
return None
else:
logger.debug(u"HTTP request made but no enpoint given.")
return None
def _http_requests_pool(self, urls, workers=10, chunk=None):
"""Generator function to request urls in chunks"""
# From cpython
if chunk is None:
chunk, extra = divmod(len(urls), workers * 4)
if extra:
chunk += 1
if len(urls) == 0:
chunk = 0
if self.ssl_verify:
session = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
else:
urllib3.disable_warnings(InsecureRequestWarning)
session = urllib3.PoolManager()
part = partial(self._http_requests_urllib3, session=session)
if len(urls) == 1:
yield part(urls[0])
else:
pool = ThreadPool(workers)
try:
for work in pool.imap_unordered(part, urls, chunk):
yield work
except Exception as e:
logger.error(u"Failed to yield request: %s" % e)
finally:
pool.close()
pool.join()
def _http_requests_urllib3(self, url, session):
"""Request the data from the url"""
try:
r = session.request(self.request_type, url, headers=self.headers, timeout=self.timeout)
except IOError as e:
logger.warn(u"Failed to access uri endpoint %s with error %s" % (self.uri, e))
return None
except Exception as e:
logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (self.uri, e))
return None
except:
logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % self.uri)
return None
response_status = r.status
response_content = r.data
response_headers = r.headers
if response_status in (200, 201):
return self._http_format_output(response_content, response_headers)
else:
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (self.uri, response_status))
return None
def _http_format_output(self, response_content, response_headers):
"""Formats the request response to the desired type"""
try:
if self.output_format == 'text':
output = response_content.decode('utf-8', 'ignore')
if self.output_format == 'dict':
output = helpers.convert_xml_to_dict(response_content.decode('utf-8', 'ignore'))
elif self.output_format == 'json':
output = helpers.convert_xml_to_json(response_content.decode('utf-8', 'ignore'))
elif self.output_format == 'xml':
output = helpers.parse_xml(response_content.decode('utf-8', 'ignore'))
else:
output = response_content
if self.callback:
return self.callback(output)
if self.return_type:
return output, response_headers['Content-Type']
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))
return None

View File

@@ -27,6 +27,66 @@ import pmsconnect
import session
def refresh_libraries():
logger.info(u"Tautulli Libraries :: Requesting libraries list refresh...")
server_id = plexpy.CONFIG.PMS_IDENTIFIER
if not server_id:
logger.error(u"Tautulli Libraries :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
return
library_sections = pmsconnect.PmsConnect().get_library_details()
if library_sections:
monitor_db = database.MonitorDatabase()
library_keys = []
new_keys = []
for section in library_sections:
section_keys = {'server_id': server_id,
'section_id': section['section_id']}
section_values = {'server_id': server_id,
'section_id': section['section_id'],
'section_name': section['section_name'],
'section_type': section['section_type'],
'thumb': section['thumb'],
'art': section['art'],
'count': section['count'],
'parent_count': section.get('parent_count', None),
'child_count': section.get('child_count', None),
}
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
library_keys.append(section['section_id'])
if result == 'insert':
new_keys.append(section['section_id'])
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
plexpy.CONFIG.write()
else:
new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
plexpy.CONFIG.write()
#if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1:
# # Start library section_id update on it's own thread
# threading.Thread(target=libraries.update_section_ids).start()
#if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1:
# # Start library labels update on it's own thread
# threading.Thread(target=libraries.update_labels).start()
logger.info(u"Tautulli Libraries :: Libraries list refreshed.")
return True
else:
logger.warn(u"Tautulli Libraries :: Unable to refresh libraries list.")
return False
def update_section_ids():
plexpy.CONFIG.UPDATE_SECTION_IDS = -1
@@ -965,7 +1025,7 @@ class Libraries(object):
monitor_db = database.MonitorDatabase()
# Refresh the PMS_URL to make sure the server_id is updated
plextv.get_real_pms_url()
plextv.get_server_resources()
server_id = plexpy.CONFIG.PMS_IDENTIFIER

View File

@@ -16,6 +16,7 @@
import arrow
import bleach
from collections import Counter
from itertools import groupby
import json
from operator import itemgetter
@@ -145,10 +146,17 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
return False
if notify_action == 'on_concurrent':
ap = activity_processor.ActivityProcessor()
user_sessions = ap.get_sessions(user_id=stream_data['user_id'],
ip_address=plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP)
return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_current_activity()
user_sessions = []
if result:
user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]
if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
return len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
else:
return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
elif notify_action == 'on_newdevice':
data_factory = datafactory.DataFactory()
@@ -504,7 +512,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
# Build Plex URL
metadata['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
web_url=plexpy.CONFIG.PMS_WEB_URL or 'https://app.plex.tv/desktop',
web_url=plexpy.CONFIG.PMS_WEB_URL,
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
rating_key=rating_key)

View File

@@ -1963,7 +1963,7 @@ class JOIN(Notifier):
deviceid_key = 'deviceId%s' % ('s' if len(self.config['device_id'].split(',')) > 1 else '')
data = {'api_key': self.config['api_key'],
data = {'apikey': self.config['api_key'],
deviceid_key: self.config['device_id'],
'text': body.encode("utf-8")}
@@ -2783,12 +2783,12 @@ class SCRIPTS(Notifier):
return False
if error:
err = '\n '.join([l for l in error.splitlines()])
err = '\n '.join([helpers.sanitize(l) for l in error.splitlines()])
logger.error(u"Tautulli Notifiers :: Script error: \n %s" % err)
return False
if output:
out = '\n '.join([l for l in output.splitlines()])
out = '\n '.join([helpers.sanitize(l) for l in output.splitlines()])
logger.debug(u"Tautulli Notifiers :: Script returned: \n %s" % out)
if not self.script_killed:

View File

@@ -23,7 +23,6 @@ import activity_processor
import database
import helpers
import logger
import plextv
import users
@@ -284,7 +283,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
# Get the latest friends list so we can pull user id's
try:
plextv.refresh_users()
users.refresh_users()
except:
logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.")
return None

View File

@@ -18,11 +18,9 @@
import base64
import json
from xml.dom import minidom
import plexpy
import common
import database
import helpers
import http_handler
import logger
@@ -31,129 +29,99 @@ import pmsconnect
import session
def refresh_users():
logger.info(u"Tautulli PlexTV :: Requesting users list refresh...")
result = PlexTV().get_full_users_list()
def get_server_resources(return_presence=False):
if not return_presence:
logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
monitor_db = database.MonitorDatabase()
user_data = users.Users()
server = {'pms_name': plexpy.CONFIG.PMS_NAME,
'pms_version': plexpy.CONFIG.PMS_VERSION,
'pms_platform': plexpy.CONFIG.PMS_PLATFORM,
'pms_ip': plexpy.CONFIG.PMS_IP,
'pms_port': plexpy.CONFIG.PMS_PORT,
'pms_ssl': plexpy.CONFIG.PMS_SSL,
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
'pms_url': plexpy.CONFIG.PMS_URL,
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL
}
if result:
for item in result:
shared_libraries = ''
user_tokens = user_data.get_tokens(user_id=item['user_id'])
if user_tokens and user_tokens['server_token']:
pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token'])
library_details = pms_connect.get_server_children()
if library_details:
shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list'])
else:
shared_libraries = ''
control_value_dict = {"user_id": item['user_id']}
new_value_dict = {"username": item['username'],
"thumb": item['thumb'],
"email": item['email'],
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"shared_libraries": shared_libraries,
"filter_all": item['filter_all'],
"filter_movies": item['filter_movies'],
"filter_tv": item['filter_tv'],
"filter_music": item['filter_music'],
"filter_photos": item['filter_photos']
}
# Check if we've set a custom avatar if so don't overwrite it.
if item['user_id']:
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
'FROM users WHERE user_id = ?',
[item['user_id']])
if avatar_urls:
if not avatar_urls[0]['custom_avatar_url'] or \
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
new_value_dict['custom_avatar_url'] = item['thumb']
else:
new_value_dict['custom_avatar_url'] = item['thumb']
monitor_db.upsert('users', new_value_dict, control_value_dict)
logger.info(u"Tautulli PlexTV :: Users list refreshed.")
return True
if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
scheme = 'https'
else:
logger.warn(u"Tautulli PlexTV :: Unable to refresh users list.")
return False
scheme = 'http'
def get_real_pms_url():
logger.info(u"Tautulli PlexTV :: Requesting URLs for server...")
# Reset any current PMS_URL value
plexpy.CONFIG.__setattr__('PMS_URL', '')
plexpy.CONFIG.write()
fallback_url = 'http://{}:{}'.format(plexpy.CONFIG.PMS_IP, plexpy.CONFIG.PMS_PORT)
fallback_url = '{scheme}://{hostname}:{port}'.format(scheme=scheme,
hostname=server['pms_ip'],
port=server['pms_port'])
plex_tv = PlexTV()
result = plex_tv.get_server_urls(include_https=plexpy.CONFIG.PMS_SSL)
plexpass = plex_tv.get_plexpass_status()
result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
pms_ip=server['pms_ip'],
pms_port=server['pms_port'],
include_https=server['pms_ssl'])
connections = []
if result:
plexpy.CONFIG.__setattr__('PMS_VERSION', result['version'])
plexpy.CONFIG.__setattr__('PMS_PLATFORM', result['platform'])
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', plexpass)
connections = result['connections']
connections = result.pop('connections', [])
server.update(result)
presence = server.pop('pms_presence', 0)
else:
connections = []
presence = 0
if return_presence:
return presence
plexpass = plex_tv.get_plexpass_status()
server['pms_plexpass'] = int(plexpass)
# Only need to retrieve PMS_URL if using SSL
if not plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL:
if not server['pms_url_manual'] and server['pms_ssl']:
if connections:
if plexpy.CONFIG.PMS_IS_REMOTE:
if server['pms_is_remote']:
# Get all remote connections
conns = [c for c in connections if c['local'] == '0' and 'plex.direct' in c['uri']]
conns = [c for c in connections if
c['local'] == '0' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])]
else:
# Get all local connections
conns = [c for c in connections if c['local'] == '1' and 'plex.direct' in c['uri']]
conns = [c for c in connections if
c['local'] == '1' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])]
if conns:
# Get connection with matching address, otherwise return first connection
conn = next((c for c in conns if c['address'] == plexpy.CONFIG.PMS_IP
and c['port'] == str(plexpy.CONFIG.PMS_PORT)), conns[0])
plexpy.CONFIG.__setattr__('PMS_URL', conn['uri'])
plexpy.CONFIG.write()
conn = next((c for c in conns if c['address'] == server['pms_ip']
and c['port'] == str(server['pms_port'])), conns[0])
server['pms_url'] = conn['uri']
logger.info(u"Tautulli PlexTV :: Server URL retrieved.")
# get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL
if not plexpy.CONFIG.PMS_URL:
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
plexpy.CONFIG.write()
if not server['pms_url']:
server['pms_url'] = fallback_url
logger.warn(u"Tautulli PlexTV :: Unable to retrieve server URLs. Using user-defined value without SSL.")
# Not using SSL, remote has no effect
# Not using SSL, remote has no effect
else:
if plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL:
fallback_url = fallback_url.replace('http://', 'https://')
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
plexpy.CONFIG.write()
server['pms_url'] = fallback_url
logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
plexpy.CONFIG.process_kwargs(server)
plexpy.CONFIG.write()
class PlexTV(object):
"""
Plex.tv authentication
"""
def __init__(self, username='', password='', token=None):
self.protocol = 'HTTPS'
def __init__(self, username=None, password=None, token=None):
self.username = username
self.password = password
self.token = token
self.urls = 'https://plex.tv'
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT
if not token:
if not self.token:
# Check if we should use the admin token, or the guest server token
if session.get_session_user_id():
user_data = users.Users()
@@ -161,12 +129,14 @@ class PlexTV(object):
self.token = user_tokens['server_token']
else:
self.token = plexpy.CONFIG.PMS_TOKEN
else:
self.token = token
self.request_handler = http_handler.HTTPHandler(host='plex.tv',
port=443,
if not self.token:
logger.error(u"Tautulli PlexTV :: PlexTV called, but no token provided.")
return
self.request_handler = http_handler.HTTPHandler(urls=self.urls,
token=self.token,
timeout=self.timeout,
ssl_verify=self.ssl_verify)
def get_plex_auth(self, output_format='raw'):
@@ -183,7 +153,6 @@ class PlexTV(object):
}
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='POST',
headers=headers,
output_format=output_format,
@@ -265,7 +234,6 @@ class PlexTV(object):
def get_plextv_friends(self, output_format=''):
uri = '/api/users'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -274,7 +242,6 @@ class PlexTV(object):
def get_plextv_user_details(self, output_format=''):
uri = '/users/account'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -283,7 +250,6 @@ class PlexTV(object):
def get_plextv_devices_list(self, output_format=''):
uri = '/devices.xml'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -292,7 +258,6 @@ class PlexTV(object):
def get_plextv_server_list(self, output_format=''):
uri = '/pms/servers.xml'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -301,7 +266,6 @@ class PlexTV(object):
def get_plextv_sync_lists(self, machine_id='', output_format=''):
uri = '/servers/%s/sync_lists' % machine_id
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -313,7 +277,6 @@ class PlexTV(object):
else:
uri = '/api/resources'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -325,7 +288,6 @@ class PlexTV(object):
else:
uri = '/api/downloads/1.json'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -334,7 +296,6 @@ class PlexTV(object):
def delete_plextv_device(self, device_id='', output_format=''):
uri = '/devices/%s.xml' % device_id
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='DELETE',
output_format=output_format)
@@ -343,7 +304,6 @@ class PlexTV(object):
def delete_plextv_device_sync_lists(self, client_id='', output_format=''):
uri = '/devices/%s/sync_items' % client_id
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -352,197 +312,184 @@ class PlexTV(object):
def delete_plextv_sync(self, client_id='', sync_id='', output_format=''):
uri = '/devices/%s/sync_items/%s' % (client_id, sync_id)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='DELETE',
output_format=output_format)
return request
def get_full_users_list(self):
friends_list = self.get_plextv_friends()
own_account = self.get_plextv_user_details()
friends_list = self.get_plextv_friends(output_format='xml')
own_account = self.get_plextv_user_details(output_format='xml')
users_list = []
try:
xml_parse = minidom.parseString(own_account)
xml_head = own_account.getElementsByTagName('user')
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account: %s" % e)
return []
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account.")
return []
logger.warn(u"Tautulli PlexTV :: Unable to parse own account XML for get_full_users_list: %s." % e)
return {}
xml_head = xml_parse.getElementsByTagName('user')
if not xml_head:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list.")
else:
for a in xml_head:
own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'username'),
"thumb": helpers.get_xml_attr(a, 'thumb'),
"email": helpers.get_xml_attr(a, 'email'),
"is_home_user": helpers.get_xml_attr(a, 'home'),
"is_allow_sync": None,
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
}
for a in xml_head:
own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'username'),
"thumb": helpers.get_xml_attr(a, 'thumb'),
"email": helpers.get_xml_attr(a, 'email'),
"is_home_user": helpers.get_xml_attr(a, 'home'),
"is_allow_sync": None,
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
}
users_list.append(own_details)
users_list.append(own_details)
try:
xml_parse = minidom.parseString(friends_list)
xml_head = friends_list.getElementsByTagName('User')
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list: %s" % e)
return []
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list.")
return []
logger.warn(u"Tautulli PlexTV :: Unable to parse friends list XML for get_full_users_list: %s." % e)
return {}
xml_head = xml_parse.getElementsByTagName('User')
if not xml_head:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list.")
else:
for a in xml_head:
friend = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'title'),
"thumb": helpers.get_xml_attr(a, 'thumb'),
"email": helpers.get_xml_attr(a, 'email'),
"is_home_user": helpers.get_xml_attr(a, 'home'),
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
}
for a in xml_head:
friend = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'title'),
"thumb": helpers.get_xml_attr(a, 'thumb'),
"email": helpers.get_xml_attr(a, 'email'),
"is_home_user": helpers.get_xml_attr(a, 'home'),
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
}
users_list.append(friend)
users_list.append(friend)
return users_list
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None, rating_key_filter=None):
sync_list = self.get_plextv_sync_lists(machine_id)
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
rating_key_filter=None, sync_id_filter=None):
if machine_id is None:
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
user_data = users.Users()
synced_items = []
try:
xml_parse = minidom.parseString(sync_list)
xml_head = sync_list.getElementsByTagName('SyncList')
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s" % e)
return []
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.")
return []
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s." % e)
return {}
xml_head = xml_parse.getElementsByTagName('SyncList')
for a in xml_head:
client_id = helpers.get_xml_attr(a, 'clientIdentifier')
if not xml_head:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.")
else:
for a in xml_head:
client_id = helpers.get_xml_attr(a, 'clientIdentifier')
# Filter by client_id
if client_id_filter and str(client_id_filter) != client_id:
continue
# Filter by client_id
if client_id_filter and client_id_filter != client_id:
continue
sync_list_id = helpers.get_xml_attr(a, 'id')
sync_device = a.getElementsByTagName('Device')
sync_id = helpers.get_xml_attr(a, 'id')
sync_device = a.getElementsByTagName('Device')
for device in sync_device:
device_user_id = helpers.get_xml_attr(device, 'userID')
try:
device_username = user_data.get_details(user_id=device_user_id)['username']
device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name']
except:
device_username = ''
device_friendly_name = ''
device_name = helpers.get_xml_attr(device, 'name')
device_product = helpers.get_xml_attr(device, 'product')
device_product_version = helpers.get_xml_attr(device, 'productVersion')
device_platform = helpers.get_xml_attr(device, 'platform')
device_platform_version = helpers.get_xml_attr(device, 'platformVersion')
device_type = helpers.get_xml_attr(device, 'device')
device_model = helpers.get_xml_attr(device, 'model')
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
for device in sync_device:
device_user_id = helpers.get_xml_attr(device, 'userID')
try:
device_username = user_data.get_details(user_id=device_user_id)['username']
device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name']
except:
device_username = ''
device_friendly_name = ''
device_name = helpers.get_xml_attr(device, 'name')
device_product = helpers.get_xml_attr(device, 'product')
device_product_version = helpers.get_xml_attr(device, 'productVersion')
device_platform = helpers.get_xml_attr(device, 'platform')
device_platform_version = helpers.get_xml_attr(device, 'platformVersion')
device_type = helpers.get_xml_attr(device, 'device')
device_model = helpers.get_xml_attr(device, 'model')
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
# Filter by user_id
if user_id_filter and str(user_id_filter) != device_user_id:
continue
# Filter by user_id
if user_id_filter and user_id_filter != device_user_id:
continue
for synced in a.getElementsByTagName('SyncItems'):
sync_item = synced.getElementsByTagName('SyncItem')
for item in sync_item:
for synced in a.getElementsByTagName('SyncItems'):
sync_item = synced.getElementsByTagName('SyncItem')
for item in sync_item:
for location in item.getElementsByTagName('Location'):
clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F')
for location in item.getElementsByTagName('Location'):
clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F')
rating_key = next((clean_uri[(idx + 1) % len(clean_uri)]
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
rating_key = next((clean_uri[(idx + 1) % len(clean_uri)]
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
# Filter by rating_key
if rating_key_filter and str(rating_key_filter) != rating_key:
continue
# Filter by rating_key
if rating_key_filter and rating_key_filter != rating_key:
continue
sync_id = helpers.get_xml_attr(item, 'id')
sync_id = helpers.get_xml_attr(item, 'id')
sync_version = helpers.get_xml_attr(item, 'version')
sync_root_title = helpers.get_xml_attr(item, 'rootTitle')
sync_title = helpers.get_xml_attr(item, 'title')
sync_metadata_type = helpers.get_xml_attr(item, 'metadataType')
sync_content_type = helpers.get_xml_attr(item, 'contentType')
# Filter by sync_id
if sync_id_filter and str(sync_id_filter) != sync_id:
continue
for status in item.getElementsByTagName('Status'):
status_failure_code = helpers.get_xml_attr(status, 'failureCode')
status_failure = helpers.get_xml_attr(status, 'failure')
status_state = helpers.get_xml_attr(status, 'state')
status_item_count = helpers.get_xml_attr(status, 'itemsCount')
status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount')
status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount')
status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount')
status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount')
status_total_size = helpers.get_xml_attr(status, 'totalSize')
status_item_download_percent_complete = helpers.get_percent(
status_item_downloaded_count, status_item_count)
sync_version = helpers.get_xml_attr(item, 'version')
sync_root_title = helpers.get_xml_attr(item, 'rootTitle')
sync_title = helpers.get_xml_attr(item, 'title')
sync_metadata_type = helpers.get_xml_attr(item, 'metadataType')
sync_content_type = helpers.get_xml_attr(item, 'contentType')
for settings in item.getElementsByTagName('MediaSettings'):
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
for status in item.getElementsByTagName('Status'):
status_failure_code = helpers.get_xml_attr(status, 'failureCode')
status_failure = helpers.get_xml_attr(status, 'failure')
status_state = helpers.get_xml_attr(status, 'state')
status_item_count = helpers.get_xml_attr(status, 'itemsCount')
status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount')
status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount')
status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount')
status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount')
status_total_size = helpers.get_xml_attr(status, 'totalSize')
status_item_download_percent_complete = helpers.get_percent(
status_item_downloaded_count, status_item_count)
sync_details = {"device_name": helpers.sanitize(device_name),
"platform": helpers.sanitize(device_platform),
"username": helpers.sanitize(device_username),
"friendly_name": helpers.sanitize(device_friendly_name),
"user_id": device_user_id,
"root_title": helpers.sanitize(sync_root_title),
"title": helpers.sanitize(sync_title),
"metadata_type": sync_metadata_type,
"content_type": sync_content_type,
"rating_key": rating_key,
"state": status_state,
"item_count": status_item_count,
"item_complete_count": status_item_complete_count,
"item_downloaded_count": status_item_downloaded_count,
"item_downloaded_percent_complete": status_item_download_percent_complete,
"music_bitrate": settings_music_bitrate,
"photo_quality": settings_photo_quality,
"video_quality": settings_video_quality,
"total_size": status_total_size,
"failure": status_failure,
"client_id": client_id,
"sync_id": sync_id
}
for settings in item.getElementsByTagName('MediaSettings'):
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
synced_items.append(sync_details)
sync_details = {"device_name": helpers.sanitize(device_name),
"platform": helpers.sanitize(device_platform),
"user_id": device_user_id,
"user": helpers.sanitize(device_friendly_name),
"username": helpers.sanitize(device_username),
"root_title": helpers.sanitize(sync_root_title),
"sync_title": helpers.sanitize(sync_title),
"metadata_type": sync_metadata_type,
"content_type": sync_content_type,
"rating_key": rating_key,
"state": status_state,
"item_count": status_item_count,
"item_complete_count": status_item_complete_count,
"item_downloaded_count": status_item_downloaded_count,
"item_downloaded_percent_complete": status_item_download_percent_complete,
"music_bitrate": settings_music_bitrate,
"photo_quality": settings_photo_quality,
"video_quality": settings_video_quality,
"total_size": status_total_size,
"failure": status_failure,
"client_id": client_id,
"sync_id": sync_id
}
synced_items.append(sync_details)
return session.filter_session_info(synced_items, filter_key='user_id')
@@ -550,27 +497,16 @@ class PlexTV(object):
logger.info(u"Tautulli PlexTV :: Deleting sync item '%s'." % sync_id)
self.delete_plextv_sync(client_id=client_id, sync_id=sync_id)
def get_server_urls(self, include_https=True):
def get_server_connections(self, pms_identifier='', pms_ip='', pms_port=32400, include_https=True):
if plexpy.CONFIG.PMS_IDENTIFIER:
server_id = plexpy.CONFIG.PMS_IDENTIFIER
else:
logger.error(u"Tautulli PlexTV :: Unable to retrieve server identity.")
if not pms_identifier:
logger.error(u"Tautulli PlexTV :: Unable to retrieve server connections: no pms_identifier provided.")
return {}
plextv_resources = self.get_plextv_resources(include_https=include_https)
plextv_resources = self.get_plextv_resources(include_https=include_https,
output_format='xml')
try:
xml_parse = minidom.parseString(plextv_resources)
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s" % e)
return {}
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls.")
return {}
try:
xml_head = xml_parse.getElementsByTagName('Device')
xml_head = plextv_resources.getElementsByTagName('Device')
except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
return {}
@@ -580,16 +516,20 @@ class PlexTV(object):
conn = []
connections = device.getElementsByTagName('Connection')
server = {"platform": helpers.get_xml_attr(device, 'platform'),
"version": helpers.get_xml_attr(device, 'productVersion')
server = {'pms_identifier': helpers.get_xml_attr(device, 'clientIdentifier'),
'pms_name': helpers.get_xml_attr(device, 'name'),
'pms_version': helpers.get_xml_attr(device, 'productVersion'),
'pms_platform': helpers.get_xml_attr(device, 'platform'),
'pms_presence': helpers.get_xml_attr(device, 'presence'),
'pms_is_cloud': 1 if helpers.get_xml_attr(device, 'platform') == 'Cloud' else 0
}
for c in connections:
server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'),
"address": helpers.get_xml_attr(c, 'address'),
"port": helpers.get_xml_attr(c, 'port'),
"uri": helpers.get_xml_attr(c, 'uri'),
"local": helpers.get_xml_attr(c, 'local')
server_details = {'protocol': helpers.get_xml_attr(c, 'protocol'),
'address': helpers.get_xml_attr(c, 'address'),
'port': helpers.get_xml_attr(c, 'port'),
'uri': helpers.get_xml_attr(c, 'uri'),
'local': helpers.get_xml_attr(c, 'local')
}
conn.append(server_details)
@@ -600,10 +540,10 @@ class PlexTV(object):
# Try to match the device
for a in xml_head:
if helpers.get_xml_attr(a, 'clientIdentifier') == server_id:
if helpers.get_xml_attr(a, 'clientIdentifier') == pms_identifier:
server = get_connections(a)
break
# Else no device match found
if not server:
# Try to match the PMS_IP and PMS_PORT
@@ -612,15 +552,8 @@ class PlexTV(object):
connections = a.getElementsByTagName('Connection')
for connection in connections:
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
plexpy.CONFIG.write()
logger.info(u"Tautulli PlexTV :: PMS identifier changed from %s to %s."
% (server_id, plexpy.CONFIG.PMS_IDENTIFIER))
if helpers.get_xml_attr(connection, 'address') == pms_ip and \
helpers.get_xml_attr(connection, 'port') == str(pms_port):
server = get_connections(a)
break
@@ -649,7 +582,7 @@ class PlexTV(object):
return server_times
def discover(self, include_cloud=True):
def discover(self, include_cloud=True, all_servers=False):
""" Query plex for all servers online. Returns the ones you own in a selectize format """
servers = self.get_plextv_resources(include_https=True, output_format='xml')
clean_servers = []
@@ -679,15 +612,16 @@ class PlexTV(object):
connections = d.getElementsByTagName('Connection')
for c in connections:
# If this is a remote server don't show any local IPs.
if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
helpers.get_xml_attr(c, 'local') == '1':
continue
if not all_servers:
# If this is a remote server don't show any local IPs.
if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
helpers.get_xml_attr(c, 'local') == '1':
continue
# If this is a local server don't show any remote IPs.
if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \
helpers.get_xml_attr(c, 'local') == '0':
continue
# If this is a local server don't show any remote IPs.
if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \
helpers.get_xml_attr(c, 'local') == '0':
continue
server = {'httpsRequired': helpers.get_xml_attr(d, 'httpsRequired'),
'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'),
@@ -770,8 +704,6 @@ class PlexTV(object):
return True
else:
logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.")
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
plexpy.CONFIG.write()
return False
def get_devices_list(self):

View File

@@ -22,7 +22,6 @@ import activity_processor
import database
import helpers
import logger
import plextv
import users
@@ -275,7 +274,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
# Get the latest friends list so we can pull user id's
try:
plextv.refresh_users()
users.refresh_users()
except:
logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.")
return None

View File

@@ -13,16 +13,12 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import threading
import urllib
from urlparse import urlparse
import plexpy
import common
import database
import helpers
import http_handler
import libraries
import logger
import plextv
import session
@@ -49,83 +45,23 @@ def get_server_friendly_name():
return server_name
def refresh_libraries():
logger.info(u"Tautulli Pmsconnect :: Requesting libraries list refresh...")
server_id = plexpy.CONFIG.PMS_IDENTIFIER
if not server_id:
logger.error(u"Tautulli Pmsconnect :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
return
library_sections = PmsConnect().get_library_details()
if library_sections:
monitor_db = database.MonitorDatabase()
library_keys = []
new_keys = []
for section in library_sections:
section_keys = {'server_id': server_id,
'section_id': section['section_id']}
section_values = {'server_id': server_id,
'section_id': section['section_id'],
'section_name': section['section_name'],
'section_type': section['section_type'],
'thumb': section['thumb'],
'art': section['art'],
'count': section['count'],
'parent_count': section.get('parent_count', None),
'child_count': section.get('child_count', None),
}
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
library_keys.append(section['section_id'])
if result == 'insert':
new_keys.append(section['section_id'])
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
plexpy.CONFIG.write()
else:
new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
plexpy.CONFIG.write()
#if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1:
# # Start library section_id update on it's own thread
# threading.Thread(target=libraries.update_section_ids).start()
#if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1:
# # Start library labels update on it's own thread
# threading.Thread(target=libraries.update_labels).start()
logger.info(u"Tautulli Pmsconnect :: Libraries list refreshed.")
return True
else:
logger.warn(u"Tautulli Pmsconnect :: Unable to refresh libraries list.")
return False
class PmsConnect(object):
"""
Retrieve data from Plex Server
"""
def __init__(self, token=None):
if plexpy.CONFIG.PMS_URL:
url_parsed = urlparse(plexpy.CONFIG.PMS_URL)
hostname = url_parsed.hostname
port = url_parsed.port
self.protocol = url_parsed.scheme
else:
hostname = plexpy.CONFIG.PMS_IP
port = plexpy.CONFIG.PMS_PORT
self.protocol = 'http'
def __init__(self, url=None, token=None):
self.url = url
self.token = token
if not token:
if not self.url and plexpy.CONFIG.PMS_URL:
self.url = plexpy.CONFIG.PMS_URL
elif not self.url:
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
port=plexpy.CONFIG.PMS_PORT)
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
if not self.token:
# Check if we should use the admin token, or the guest server token
if session.get_session_user_id():
user_data = users.Users()
@@ -133,12 +69,10 @@ class PmsConnect(object):
self.token = user_tokens['server_token']
else:
self.token = plexpy.CONFIG.PMS_TOKEN
else:
self.token = token
self.request_handler = http_handler.HTTPHandler(host=hostname,
port=port,
token=self.token)
self.request_handler = http_handler.HTTPHandler(urls=self.url,
token=self.token,
timeout=self.timeout)
def get_sessions(self, output_format=''):
"""
@@ -150,7 +84,6 @@ class PmsConnect(object):
"""
uri = '/status/sessions'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -166,7 +99,6 @@ class PmsConnect(object):
"""
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, reason)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -183,7 +115,6 @@ class PmsConnect(object):
"""
uri = '/library/metadata/' + rating_key
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -200,7 +131,6 @@ class PmsConnect(object):
"""
uri = '/library/metadata/' + rating_key + '/children'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -217,7 +147,6 @@ class PmsConnect(object):
"""
uri = '/library/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (start, count)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -234,7 +163,6 @@ class PmsConnect(object):
"""
uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (section_id, start, count)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -250,6 +178,22 @@ class PmsConnect(object):
Output: array
"""
uri = '/library/metadata/' + rating_key + '/children'
request = self.request_handler.make_request(uri=uri,
request_type='GET',
output_format=output_format)
return request
def get_children_list_related(self, rating_key='', output_format=''):
"""
Return list of related children in requested collection item.
Parameters required: rating_key { ratingKey of parent }
Optional parameters: output_format { dict, json }
Output: array
"""
uri = '/hubs/metadata/' + rating_key + '/related'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
@@ -268,7 +212,6 @@ class PmsConnect(object):
"""
uri = '/library/metadata/' + rating_key + '/allLeaves'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -284,7 +227,6 @@ class PmsConnect(object):
"""
uri = '/servers'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -300,7 +242,6 @@ class PmsConnect(object):
"""
uri = '/:/prefs'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -316,7 +257,6 @@ class PmsConnect(object):
"""
uri = '/identity'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -332,7 +272,6 @@ class PmsConnect(object):
"""
uri = '/library/sections'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -351,7 +290,6 @@ class PmsConnect(object):
uri = '/library/sections/' + section_id + '/' + list_type + '?X-Plex-Container-Start=0' + count + sort_type + label_key
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -367,7 +305,6 @@ class PmsConnect(object):
"""
uri = '/library/sections/' + section_id + '/label'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -384,7 +321,6 @@ class PmsConnect(object):
"""
uri = '/sync/items/' + sync_id
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -400,7 +336,6 @@ class PmsConnect(object):
"""
uri = '/sync/transcodeQueue'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -414,9 +349,8 @@ class PmsConnect(object):
Output: array
"""
uri = '/hubs/search?query=' + urllib.quote(query.encode('utf8')) + '&limit=' + limit
uri = '/hubs/search?query=' + urllib.quote(query.encode('utf8')) + '&limit=' + limit + '&includeCollections=1'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -432,7 +366,6 @@ class PmsConnect(object):
"""
uri = '/myplex/account'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -448,7 +381,6 @@ class PmsConnect(object):
"""
uri = '/myplex/refreshReachability'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='PUT')
return request
@@ -463,7 +395,6 @@ class PmsConnect(object):
"""
uri = '/updater/check?download=0'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='PUT',
output_format=output_format)
@@ -479,7 +410,6 @@ class PmsConnect(object):
"""
uri = '/updater/status'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -498,7 +428,6 @@ class PmsConnect(object):
"""
uri = '/hubs/home/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s&type=%s' % (start, count, type)
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
@@ -607,7 +536,7 @@ class PmsConnect(object):
try:
xml_head = metadata.getElementsByTagName('MediaContainer')
except Exception as e:
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata: %s." % e)
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata_details: %s." % e)
return {}
metadata = {}
@@ -1039,6 +968,50 @@ class PmsConnect(object):
helpers.get_xml_attr(metadata_main, 'title'))
}
elif metadata_type == 'collection':
metadata = {'media_type': metadata_type,
'sub_media_type': helpers.get_xml_attr(metadata_main, 'subtype'),
'section_id': section_id,
'library_name': library_name,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
'title': helpers.get_xml_attr(metadata_main, 'title'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
'min_year': helpers.get_xml_attr(metadata_main, 'minYear'),
'max_year': helpers.get_xml_attr(metadata_main, 'maxYear'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb').split('?')[0],
'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': helpers.get_xml_attr(metadata_main, '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'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'child_count': helpers.get_xml_attr(metadata_main, 'childCount'),
'directors': directors,
'writers': writers,
'actors': actors,
'genres': genres,
'labels': labels,
'full_title': helpers.get_xml_attr(metadata_main, 'title')
}
elif metadata_type == 'clip':
metadata = {'media_type': metadata_type,
'section_id': section_id,
@@ -1434,11 +1407,11 @@ class PmsConnect(object):
transcode_decision = 'direct play'
# Determine if a synced version is being played
sync_id = None
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
plex_tv = plextv.PlexTV()
synced_items = plex_tv.get_synced_items(machine_id=plexpy.CONFIG.PMS_IDENTIFIER,
client_id_filter=player_details['machine_id'],
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
rating_key_filter=rating_key)
if synced_items:
sync_id = synced_items[0]['sync_id']
@@ -1448,8 +1421,6 @@ class PmsConnect(object):
synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0]
elif synced_xml_head[0].getElementsByTagName('Video'):
synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0]
else:
sync_id = None
# Figure out which version is being played
if sync_id:
@@ -1796,7 +1767,6 @@ class PmsConnect(object):
else:
return False
def get_item_children(self, rating_key=''):
"""
Return processed and validated children list.
@@ -1837,8 +1807,11 @@ class PmsConnect(object):
for result in result_data:
children_output = {'section_id': section_id,
'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(result, 'parentRatingKey'),
'media_index': helpers.get_xml_attr(result, 'index'),
'title': helpers.get_xml_attr(result, 'title'),
'parent_title': helpers.get_xml_attr(result, 'parentTitle'),
'year': helpers.get_xml_attr(result, 'year'),
'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration')
@@ -1853,6 +1826,72 @@ class PmsConnect(object):
return output
def get_item_children_related(self, rating_key=''):
"""
Return processed and validated children list.
Output: array
"""
children_data = self.get_children_list_related(rating_key, output_format='xml')
try:
xml_head = children_data.getElementsByTagName('MediaContainer')
except Exception as e:
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_item_children_related: %s." % e)
return []
children_results_list = {'movie': [],
'show': [],
'season': [],
'episode': [],
'artist': [],
'album': [],
'track': [],
}
for a in xml_head:
section_id = helpers.get_xml_attr(a, 'librarySectionID')
hubs = a.getElementsByTagName('Hub')
for h in hubs:
size = helpers.get_xml_attr(h, 'size')
media_type = helpers.get_xml_attr(h, 'type')
title = helpers.get_xml_attr(h, 'title')
hub_identifier = helpers.get_xml_attr(h, 'hubIdentifier')
if size == '0' or not hub_identifier.startswith('collection.related') or \
media_type not in children_results_list.keys():
continue
result_data = []
if h.getElementsByTagName('Video'):
result_data = h.getElementsByTagName('Video')
if h.getElementsByTagName('Directory'):
result_data = h.getElementsByTagName('Directory')
if h.getElementsByTagName('Track'):
result_data = h.getElementsByTagName('Track')
for result in result_data:
children_output = {'section_id': section_id,
'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(result, 'parentRatingKey'),
'media_index': helpers.get_xml_attr(result, 'index'),
'title': helpers.get_xml_attr(result, 'title'),
'parent_title': helpers.get_xml_attr(result, 'parentTitle'),
'year': helpers.get_xml_attr(result, 'year'),
'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration')
}
children_results_list[media_type].append(children_output)
output = {'results_count': sum(len(s) for s in children_results_list.items()),
'results_list': children_results_list,
}
return output
def get_servers_info(self):
"""
Return the list of local servers.
@@ -2206,7 +2245,6 @@ class PmsConnect(object):
uri = '/photo/:/transcode?%s' % urllib.urlencode(params)
result = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
return_type=True)
@@ -2238,7 +2276,8 @@ class PmsConnect(object):
'episode': [],
'artist': [],
'album': [],
'track': []
'track': [],
'collection': []
}
for a in xml_head:

View File

@@ -23,9 +23,67 @@ import datatables
import helpers
import logger
import plextv
import pmsconnect
import session
def refresh_users():
logger.info(u"Tautulli Users :: Requesting users list refresh...")
result = plextv.PlexTV().get_full_users_list()
monitor_db = database.MonitorDatabase()
user_data = Users()
if result:
for item in result:
shared_libraries = ''
user_tokens = user_data.get_tokens(user_id=item['user_id'])
if user_tokens and user_tokens['server_token']:
pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token'])
library_details = pms_connect.get_server_children()
if library_details:
shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list'])
else:
shared_libraries = ''
control_value_dict = {"user_id": item['user_id']}
new_value_dict = {"username": item['username'],
"thumb": item['thumb'],
"email": item['email'],
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"shared_libraries": shared_libraries,
"filter_all": item['filter_all'],
"filter_movies": item['filter_movies'],
"filter_tv": item['filter_tv'],
"filter_music": item['filter_music'],
"filter_photos": item['filter_photos']
}
# Check if we've set a custom avatar if so don't overwrite it.
if item['user_id']:
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
'FROM users WHERE user_id = ?',
[item['user_id']])
if avatar_urls:
if not avatar_urls[0]['custom_avatar_url'] or \
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
new_value_dict['custom_avatar_url'] = item['thumb']
else:
new_value_dict['custom_avatar_url'] = item['thumb']
monitor_db.upsert('users', new_value_dict, control_value_dict)
logger.info(u"Tautulli Users :: Users list refreshed.")
return True
else:
logger.warn(u"Tautulli Users :: Unable to refresh users list.")
return False
class Users(object):
def __init__(self):
@@ -360,7 +418,7 @@ class Users(object):
logger.warn(u"Tautulli Users :: Unable to retrieve user %s from database. Requesting user list refresh."
% user_id if user_id else user)
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
plextv.refresh_users()
refresh_users()
user_details = get_user_details(user_id=user_id, user=user)

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.0.2-beta"
PLEXPY_RELEASE_VERSION = "v2.0.5-beta"

View File

@@ -175,10 +175,20 @@ def checkGithub(auto_update=False):
if plexpy.COMMITS_BEHIND > 0:
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
url = 'https://api.github.com/repos/%s/plexpy/releases/latest' % plexpy.CONFIG.GIT_USER
release = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
url = 'https://api.github.com/repos/%s/plexpy/releases' % plexpy.CONFIG.GIT_USER
releases = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
if plexpy.CONFIG.GIT_BRANCH == 'master':
release = next((r for r in releases if not r['prerelease']), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
release = next((r for r in releases if r['prerelease'] and '-beta' in r['tag_name']), releases[0])
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
release = next((r for r in releases if r['prerelease'] and '-nightly' in r['tag_name']), releases[0])
else:
release = releases[0]
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', 'plexpy_download_info': release,
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})
if auto_update:
logger.info('Running automatic update.')

View File

@@ -42,6 +42,7 @@ def start_thread():
def on_disconnect():
activity_processor.ActivityProcessor().set_temp_stopped()
plexpy.initialize_scheduler()
def reconnect():
@@ -148,9 +149,8 @@ def run():
logger.info(u"Tautulli WebSocket :: Unable to get an internal response from the server, Plex server is down.")
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
plexpy.PLEX_SERVER_UP = False
on_disconnect()
plexpy.initialize_scheduler()
on_disconnect()
logger.debug(u"Tautulli WebSocket :: Leaving thread.")

View File

@@ -27,9 +27,8 @@ from hashing_passwords import check_hash
import plexpy
import logger
import plextv
from plexpy.database import MonitorDatabase
from plexpy.users import Users
from plexpy.users import Users, refresh_users
from plexpy.plextv import PlexTV
@@ -72,7 +71,7 @@ def user_login(username=None, password=None):
if result:
# Refresh the users list to make sure we have all the correct permissions.
plextv.refresh_users()
refresh_users()
# Successful login
return True
else:
@@ -244,7 +243,6 @@ class AuthController(object):
expiry = datetime.now() + (timedelta(days=30) if remember_me == '1' else timedelta(minutes=60))
cherrypy.session.regenerate()
cherrypy.request.login = username
cherrypy.session[SESSION_KEY] = {'user_id': user_id,
'user': username,

View File

@@ -16,10 +16,8 @@
import hashlib
import json
import os
import random
import shutil
import threading
import uuid
import cherrypy
from cherrypy.lib.static import serve_file, serve_download
@@ -119,7 +117,7 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi("get_server_list")
def discover(self, token=None, include_cloud=True, **kwargs):
def discover(self, token=None, include_cloud=True, all_servers=False, **kwargs):
""" Get all your servers that are published to Plex.tv.
```
@@ -150,12 +148,14 @@ class WebInterface(object):
plexpy.CONFIG.write()
include_cloud = not (include_cloud == 'false')
all_servers = all_servers == 'true'
plex_tv = plextv.PlexTV()
servers = plex_tv.discover(include_cloud=include_cloud)
servers_list = plex_tv.discover(include_cloud=include_cloud,
all_servers=all_servers)
if servers:
return servers
if servers_list:
return servers_list
##### Home #####
@@ -170,6 +170,7 @@ class WebInterface(object):
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
"home_stats_recently_added_count": plexpy.CONFIG.HOME_STATS_RECENTLY_ADDED_COUNT,
"pms_name": plexpy.CONFIG.PMS_NAME,
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG
}
return serve_template(templatename="index.html", title="Home", config=config)
@@ -262,6 +263,12 @@ class WebInterface(object):
else:
return {'result': 'error', 'message': 'Failed to terminate session.'}
@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
@cherrypy.expose
@requireAuth()
def home_stats(self, time_range=30, stats_type=0, stats_count=10, **kwargs):
@@ -452,7 +459,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def refresh_libraries_list(self, **kwargs):
""" Refresh the libraries list on it's own thread. """
threading.Thread(target=pmsconnect.refresh_libraries).start()
threading.Thread(target=libraries.refresh_libraries).start()
logger.info(u"Manual libraries list refresh requested.")
return True
@@ -1074,7 +1081,7 @@ class WebInterface(object):
@requireAuth(member_of("admin"))
def refresh_users_list(self, **kwargs):
""" Refresh the users list on it's own thread. """
threading.Thread(target=plextv.refresh_users).start()
threading.Thread(target=users.refresh_users).start()
logger.info(u"Manual users list refresh requested.")
return True
@@ -2559,6 +2566,8 @@ class WebInterface(object):
"pms_port": plexpy.CONFIG.PMS_PORT,
"pms_token": plexpy.CONFIG.PMS_TOKEN,
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
"pms_uuid": plexpy.CONFIG.PMS_UUID,
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL,
@@ -2576,7 +2585,6 @@ class WebInterface(object):
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
"refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
"notify_consecutive": checked(plexpy.CONFIG.NOTIFY_CONSECUTIVE),
"notify_upload_posters": checked(plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS),
"notify_recently_added_upgrade": checked(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE),
@@ -2733,8 +2741,7 @@ class WebInterface(object):
# Get new server URLs for SSL communications and get new server friendly name
if server_changed:
plextv.get_real_pms_url()
pmsconnect.get_server_friendly_name()
plextv.get_server_resources()
web_socket.reconnect()
# If first run, start websocket
@@ -2751,11 +2758,11 @@ class WebInterface(object):
# Refresh users table if our server IP changes.
if refresh_libraries:
threading.Thread(target=pmsconnect.refresh_libraries).start()
threading.Thread(target=libraries.refresh_libraries).start()
# Refresh users table if our server IP changes.
if refresh_users:
threading.Thread(target=plextv.refresh_users).start()
threading.Thread(target=users.refresh_users).start()
return {'result': 'success', 'message': 'Settings saved.'}
@@ -3425,16 +3432,15 @@ class WebInterface(object):
# Fallback to checking /identity endpoint is server is unpublished
# Cannot set SSL settings on the PMS if unpublished so 'http' is okay
if not identifier:
request_handler = http_handler.HTTPHandler(host=hostname,
port=port,
token=None)
scheme = 'https' if ssl else 'http'
url = '{scheme}://{hostname}:{port}'.format(scheme=scheme, hostname=hostname, port=port)
uri = '/identity'
request_handler = http_handler.HTTPHandler(urls=url,
ssl_verify=False)
request = request_handler.make_request(uri=uri,
proto='http',
request_type='GET',
output_format='xml',
no_token=True,
timeout=10)
output_format='xml')
if request:
xml_head = request.getElementsByTagName('MediaContainer')[0]
identifier = xml_head.getAttribute('machineIdentifier')
@@ -3588,7 +3594,7 @@ class WebInterface(object):
def get_item_children(self, rating_key='', **kwargs):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_item_children(rating_key)
result = pms_connect.get_item_children(rating_key=rating_key)
if result:
return serve_template(templatename="info_children_list.html", data=result, title="Children List")
@@ -3596,6 +3602,18 @@ class WebInterface(object):
logger.warn(u"Unable to retrieve data for get_item_children.")
return serve_template(templatename="info_children_list.html", data=None, title="Children List")
@cherrypy.expose
@requireAuth()
def get_item_children_related(self, rating_key='', title='', **kwargs):
pms_connect = pmsconnect.PmsConnect()
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)
else:
return serve_template(templatename="info_collection_list.html", data=None, title=title)
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth()
@@ -4422,7 +4440,8 @@ class WebInterface(object):
counts = {'stream_count_direct_play': 0,
'stream_count_direct_stream': 0,
'stream_count_transcode': 0}
'stream_count_transcode': 0,
'total_bandwidth': 0}
for s in result['sessions']:
if s['transcode_decision'] == 'transcode':
@@ -4432,6 +4451,8 @@ class WebInterface(object):
else:
counts['stream_count_direct_play'] += 1
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
result.update(counts)
return result

View File

@@ -69,14 +69,14 @@ def initialize(options):
if options['http_password']:
logger.info(u"Tautulli WebStart :: Web server authentication is enabled, username is '%s'", options['http_username'])
if options['http_basic_auth']:
auth_enabled = session_enabled = False
session_enabled = auth_enabled = False
basic_auth_enabled = True
else:
options_dict['tools.sessions.on'] = auth_enabled = session_enabled = True
options_dict['tools.sessions.on'] = session_enabled = auth_enabled = True
basic_auth_enabled = False
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
else:
auth_enabled = session_enabled = basic_auth_enabled = False
session_enabled = auth_enabled = basic_auth_enabled = False
if options['http_root'].strip('/'):
plexpy.HTTP_ROOT = options['http_root'] = '/' + options['http_root'].strip('/') + '/'
@@ -93,9 +93,12 @@ def initialize(options):
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
'text/javascript', 'application/json',
'application/javascript'],
'tools.auth.on': auth_enabled,
'tools.sessions.on': session_enabled,
'tools.session.name': 'tautulli_session_id-' + plexpy.CONFIG.PMS_UUID,
'tools.sessions.storage_type': 'file',
'tools.sessions.storage_path': plexpy.CONFIG.CACHE_DIR,
'tools.sessions.timeout': 30 * 24 * 60, # 30 days
'tools.auth.on': auth_enabled,
'tools.auth_basic.on': basic_auth_enabled,
'tools.auth_basic.realm': 'Tautulli web server',
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
@@ -112,8 +115,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
},
'/images': {
'tools.staticdir.on': True,
@@ -123,8 +126,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
},
'/css': {
'tools.staticdir.on': True,
@@ -134,8 +137,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
},
'/fonts': {
'tools.staticdir.on': True,
@@ -145,8 +148,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
},
'/js': {
'tools.staticdir.on': True,
@@ -156,8 +159,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
},
'/cache': {
'tools.staticdir.on': True,
@@ -167,8 +170,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
},
#'/pms_image_proxy': {
# 'tools.staticdir.on': True,
@@ -189,8 +192,8 @@ def initialize(options):
'tools.caching.delay': 0,
'tools.expires.on': True,
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
'tools.auth.on': False,
'tools.sessions.on': False
'tools.sessions.on': False,
'tools.auth.on': False
}
}