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 # 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: * Monitoring:
* Fix: Websocket connection fails to start with existing streams when upgrading to v2. * Fix: Websocket connection fails to start with existing streams when upgrading to v2.

View File

@@ -20,26 +20,21 @@
${next.headIncludes()} ${next.headIncludes()}
<!-- Favicons --> <!-- 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="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.1"> <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.1"> <link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS --> <!-- ICONS -->
<!-- Android --> <!-- 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.5">
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.1">
<meta name="theme-color" content="#282a2d"> <meta name="theme-color" content="#282a2d">
<!-- Apple --> <!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.1"> <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.1" color="#282a2d"> <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-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 --> <!-- Microsoft -->
<meta name="application-name" content="Tautulli"> <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> </head>
<body class="content"> <body class="content">
@@ -69,8 +64,8 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="home"> <a class="navbar-brand svg" href="home">
<img alt="Tautulli" src="${http_root}images/logo-tautulli.svg" height="45"> <object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object>
</a> </a>
</div> </div>
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1"> <div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">

View File

@@ -74,11 +74,11 @@ DOCUMENTATION :: END
<tr> <tr>
<td class="top-line">Resources:</td> <td class="top-line">Resources:</td>
<td class="top-line"> <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('https://github.com/%s/plexpy/issues' % plexpy.CONFIG.GIT_USER)}" data-id="issue">GitHub Issues</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> | <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>
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -112,7 +112,6 @@ DOCUMENTATION :: END
$('.guidelines-modal-link').on('click', function (e) { $('.guidelines-modal-link').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$('#guidelines-link').attr('href', $('#source-link').attr('href'));
$('#guidelines-type').text($(this).data('id')) $('#guidelines-type').text($(this).data('id'))
$('#guidelines-modal').modal(); $('#guidelines-modal').modal();
$('#guidelines-continue').attr('href', $(this).attr('href')).on('click', function () { $('#guidelines-continue').attr('href', $(this).attr('href')).on('click', function () {
@@ -121,7 +120,6 @@ DOCUMENTATION :: END
}); });
$('.support-modal-link').on('click', function (e) { $('.support-modal-link').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$('#faq-link').attr('href', $('#faq-source-link').attr('href'));
$('#support-modal').modal(); $('#support-modal').modal();
$('#support-continue').attr('href', $(this).attr('href')).on('click', function () { $('#support-continue').attr('href', $(this).attr('href')).on('click', function () {
$('#support-modal').modal('hide'); $('#support-modal').modal('hide');

View File

@@ -13,6 +13,18 @@ a:focus {
text-decoration: none; text-decoration: none;
outline: 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 { select, .react-selectize.bootstrap3.root-node .react-selectize-control {
margin: 5px 0 5px 0; margin: 5px 0 5px 0;
border: 2px solid #444; border: 2px solid #444;
@@ -841,6 +853,15 @@ a .users-poster-face:hover {
-webkit-flex-grow: 1; -webkit-flex-grow: 1;
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 { .dashboard-activity-info-time {
position: absolute; position: absolute;
top: 200px; top: 200px;
@@ -1808,7 +1829,8 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
margin-left: 2px; margin-left: 2px;
color: #999; color: #999;
} }
#children-list, #search-results-list { .children-list,
.search-results-list {
position: relative; position: relative;
z-index: 0; 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); box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
z-index: -2; z-index: -2;
} }
.item-children-poster-face.season-poster { .item-children-poster-face.poster-item {
width: 150px; width: 150px;
height: 225px; height: 225px;
} }
.item-children-poster-face.episode-poster { .item-children-poster-face.episode-item {
width: 250px; width: 250px;
height: 140px; height: 140px;
} }
.item-children-poster-face.album-poster { .item-children-poster-face.cover-item {
width: 150px; width: 150px;
height: 150px; height: 150px;
} }
@@ -1915,15 +1937,13 @@ a:hover .item-children-poster {
margin-bottom: 20px; margin-bottom: 20px;
clear: both; 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; width: 150px;
} }
.item-children-instance-text-wrapper.episode-item { .item-children-instance-text-wrapper.episode-item {
width: 250px; width: 250px;
} }
.item-children-instance-text-wrapper.album-item {
width: 150px;
}
.item-children-instance-text-wrapper h3 { .item-children-instance-text-wrapper h3 {
width: 100%; width: 100%;
padding: 5px 3px 0 3px; padding: 5px 3px 0 3px;
@@ -3699,3 +3719,74 @@ a:hover .overlay-refresh-image:hover {
.no-image { .no-image {
background-image: none !important; 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"> <ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
% if _session['user_group'] == 'admin': % 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: % else:
<div class="sub-heading">Stream</div> <div class="sub-heading">Stream</div>
% endif % endif
@@ -272,9 +272,9 @@ DOCUMENTATION :: END
<div class="sub-heading">Location</div> <div class="sub-heading">Location</div>
<div class="sub-value"> <div class="sub-value">
% if data['ip_address'] != 'N/A': % 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']}"> <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> </a>
<script> <script>
isPrivateIP("${data['ip_address']}").then(function () { isPrivateIP("${data['ip_address']}").then(function () {
@@ -301,7 +301,7 @@ DOCUMENTATION :: END
bw = str(bw) + ' kbps' bw = str(bw) + ' kbps'
%> %>
<span id="stream-bandwidth-${sk}">${bw}</span> <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: % elif data['synced_version'] == 1 or data['channel_stream'] == 1:
<span id="stream-bandwidth-${sk}">None</span> <span id="stream-bandwidth-${sk}">None</span>
% else: % 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> <browserconfig>
<msapplication> <msapplication>
<tile> <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.5"/>
<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"/>
<TileColor>#282a2d</TileColor> <TileColor>#282a2d</TileColor>
</tile> </tile>
</msapplication> </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", "name": "Tautulli",
"icons": [ "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", "sizes": "192x192",
"type": "image/png" "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", "sizes": "256x256",
"type": "image/png" "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) --> <!-- 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"> <!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" <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="Layer_1">
<g id="Logo"> <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
<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)"> C408.665,178.818,405.017,191.064,401.343,203.396z"/>
</image> <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
<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)"> 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
</image> 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> </g>
<g id="Layer_2"> <g id="Layer_2_1_">
<g> <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"/> <image overflow="visible" opacity="0.2" width="279" height="216" xlink:href="13AD46AC.png" transform="matrix(1 0 0 1 15.5 73.5)">
<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 </image>
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 <g>
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 <path fill="#FFFFFF" d="M243.77,212.601c-10.482,0-19.533,6.118-23.783,14.977l-37.755-5.478
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 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
C118.165,171.293,111.371,174.477,103.83,174.477z"/> 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
<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.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.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 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
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"/> c14.56,0,26.363-11.804,26.363-26.362S258.33,212.601,243.77,212.601z"/>
<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 </g>
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 </g>
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 <g>
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 <image overflow="visible" opacity="0.2" width="279" height="279" xlink:href="13AD46AD.png" transform="matrix(1 0 0 1 15.5 10.5)">
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 </image>
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"/> <g>
<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 <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-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 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
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 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
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 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
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 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
c3.837,12.59,7.567,24.82,11.333,37.175C364.179,165.396,356.836,165.396,349.343,165.396z"/> c14.561,0,26.363-11.802,26.363-26.361C270.135,46.582,258.33,34.779,243.77,34.779z"/>
<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 </g>
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"/>
</g> </g>
</g> </g>
</svg> </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"> <!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" <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"> 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)"> <g>
</image>
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.376,6.382-24.809,15.623l-39.383-5.715 <image overflow="visible" opacity="0.2" width="289" height="224" xlink:href="93124B54.png" transform="matrix(1 0 0 1 4.9805 70.5)">
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 </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 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 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.464,5.727c0.229,14.99,12.443,27.074,27.489,27.074c15.188,0,27.5-12.313,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"/> 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)"> </g>
</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.835,19.216l-60.943,86.113 <g>
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
<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 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-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.836,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,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"/> S256.998,29.75,241.811,29.75z"/>
</g>
</g>
</svg> </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) --> <!-- 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"> <!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" <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"> xml:space="preserve">
<g> <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 <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
C5.548,375.804-0.747,396.967-7.087,418.282z"/> C177.798,94.804,171.503,115.967,165.163,137.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 <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.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 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 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 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 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.253C18.52,418.282,5.846,418.282-7.087,418.282z"/> 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="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 <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.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 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
C-59.334,347.68-59.334,339.04-59.334,330.243z"/> C112.916,66.68,112.916,58.04,112.916,49.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 <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-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 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.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-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.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 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 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"/> C391.734,51.463,391.734,50.502,391.734,49.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 <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.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 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.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-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.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 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.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 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.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 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
C488.578,395.852,488.595,364.823,488.595,333.793z"/> C660.828,114.852,660.845,83.823,660.845,52.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 <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.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 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
C236.383,339.288,236.383,347.853,236.383,356.745z"/> C408.633,58.288,408.633,66.853,408.633,75.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 <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.654,0C552.24,412.659,552.24,371.556,552.24,330.454z"/> 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="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 <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,0C664.878,412.618,664.878,371.515,664.878,330.476z"/> 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="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 <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
C746.886,380.631,746.886,430.583,746.886,480.62z"/> C919.136,99.631,919.136,149.583,919.136,199.62z"/>
<circle fill="#E5A00D" cx="762.501" cy="290.417" r="21.5"/> <circle fill="#E5A00D" cx="934.751" cy="9.417" r="21.5"/>
</g> </g>
</svg> </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="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="padded-header" id="current-activity-header"> <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>
<div id="currentActivity"> <div id="currentActivity">
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div> <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/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.scrollbar.min.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.mousewheel.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
<script> <script>
var date_format = 'YYYY-MM-DD'; var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a'; 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() { function getCurrentActivity() {
activity_ready = false;
$.ajax({ $.ajax({
url: 'get_activity', url: 'get_activity',
type: 'GET', type: 'GET',
@@ -261,7 +277,7 @@
% else: % else:
var msg_settings = '' var msg_settings = ''
% endif % 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>'); $('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
return return
} }
@@ -275,20 +291,25 @@
// Update the header stream counts // Update the header stream counts
var sc_dp = current_activity.stream_count_direct_play, var sc_dp = current_activity.stream_count_direct_play,
sc_ds = current_activity.stream_count_direct_stream, sc_ds = current_activity.stream_count_direct_stream,
sc_tc = current_activity.stream_count_transcode; sc_tc = current_activity.stream_count_transcode,
var header_count = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' ('; total_bw = current_activity.total_bandwidth;
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
if (sc_dp) { 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) { 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) { 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(/, $/, ''); streams_header = streams_header.replace(/, $/, '') + ')';
header_count += ')'; $('#currentActivityHeader-streams').text(streams_header);
$('#currentActivityHeader').text(header_count);
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) { sessions.forEach(function (session) {
var s = new Proxy(session, defaultHandler); var s = new Proxy(session, defaultHandler);
@@ -298,6 +319,7 @@
// Create a new instance if it doesn't exist // Create a new instance if it doesn't exist
if (!(instance.length)) { if (!(instance.length)) {
create_instances.push(key);
getActivityInstance(key); getActivityInstance(key);
return; return;
} }
@@ -498,9 +520,11 @@
}); });
} else { } else {
$('#currentActivityHeader').text(''); $('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>'); $('#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 }); $('#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 }); $('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller'); 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(); getCurrentActivity();
setInterval(function () { setInterval(function () {
if (!(create_instances.length) && activity_ready) {
getCurrentActivity(); getCurrentActivity();
}
}, 2000); }, 2000);
setInterval(function(){ setInterval(function(){
@@ -617,6 +648,12 @@
}); });
}); });
}); });
$('#sessions-shortcut').on('tripleclick', function () {
$.getJSON('return_sessions_url', function(sessions_url) {
window.open(sessions_url, '_blank');
});
});
% endif % endif
</script> </script>
% endif % endif

View File

@@ -80,7 +80,7 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="summary-navbar-list"> <div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb"> <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><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['media_type'] == 'show': % elif data['media_type'] == 'show':
@@ -116,9 +116,9 @@ DOCUMENTATION :: END
<div class="col-md-9"> <div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm"> <div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track': % 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: % 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 % endif
% if data['media_type'] == 'episode': % 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);"> <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> </a>
</div> </div>
<div class="summary-content-title"> <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> <h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['media_type'] == 'season': % elif data['media_type'] == 'season':
<h1>&nbsp;</h1><h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1> <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"> <div class="col-md-9">
% if data['media_type'] == 'movie': % if data['media_type'] == 'movie':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;"> <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;"> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;"> <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> Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['media_type'] == 'album' or data['media_type'] == 'track': % elif data['media_type'] == 'album' or data['media_type'] == 'track':
Released <strong> ${data['year']}</strong> Released <strong> ${data['year']}</strong>
% elif data['media_type'] == 'collection':
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
% endif % endif
</div> </div>
<div class="summary-content-details-tag"> <div class="summary-content-details-tag">
@@ -308,51 +310,65 @@ DOCUMENTATION :: END
</div> </div>
% if data['media_type'] == 'show': % if data['media_type'] == 'show':
<div class="col-md-12"> <div class="col-md-12">
<div class='table-card-header'> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
<span>Season List for <strong>${data['title']}</strong></span> <span>Season List for <strong>${data['title']}</strong></span>
</div> </div>
</div> </div>
<div class='table-card-back'> <div class="table-card-back">
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div> <div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
</div> </div>
</div> </div>
% elif data['media_type'] == 'season': % elif data['media_type'] == 'season':
<div class="col-md-12"> <div class="col-md-12">
<div class='table-card-header'> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
<span>Episode List for <strong>${data['title']}</strong></span> <span>Episode List for <strong>${data['title']}</strong></span>
</div> </div>
</div> </div>
<div class='table-card-back'> <div class="table-card-back">
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div> <div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
</div> </div>
</div> </div>
% elif data['media_type'] == 'artist': % elif data['media_type'] == 'artist':
<div class="col-md-12"> <div class="col-md-12">
<div class='table-card-header'> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
<span>Album List for <strong>${data['title']}</strong></span> <span>Album List for <strong>${data['title']}</strong></span>
</div> </div>
</div> </div>
<div class='table-card-back'> <div class="table-card-back">
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div> <div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
</div> </div>
</div> </div>
% elif data['media_type'] == 'album': % elif data['media_type'] == 'album':
<div class="col-md-12"> <div class="col-md-12">
<div class='table-card-header'> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
<span>Track List for <strong>${data['title']}</strong></span> <span>Track List for <strong>${data['title']}</strong></span>
</div> </div>
</div> </div>
<div class='table-card-back'> <div class="table-card-back">
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div> <div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div>
</div> </div>
</div> </div>
% elif data['media_type'] == 'collection':
<div class="col-md-12">
<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 % endif
% if data['media_type'] != 'collection':
<div class="col-md-12"> <div class="col-md-12">
<div class='table-card-header'> <div class="table-card-header">
<div class="header-bar"> <div class="header-bar">
<span>Watch History for <strong>${data['title']}</strong></span> <span>Watch History for <strong>${data['title']}</strong></span>
</div> </div>
@@ -420,6 +436,7 @@ DOCUMENTATION :: END
</table> </table>
</div> </div>
</div> </div>
% endif
</div> </div>
</div> </div>
</div> </div>
@@ -562,6 +579,7 @@ DOCUMENTATION :: END
} }
</script> </script>
% endif % endif
% if data['media_type'] != 'collection':
<script> <script>
$(document).ready(function () { $(document).ready(function () {
get_history(); get_history();
@@ -638,7 +656,8 @@ DOCUMENTATION :: END
}); });
}); });
</script> </script>
% if data['media_type'] in ('show', 'season', 'artist', 'album'): % endif
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
<script> <script>
$.ajax({ $.ajax({
url: 'get_item_children', url: 'get_item_children',
@@ -646,7 +665,24 @@ DOCUMENTATION :: END
async: true, async: true,
data: { rating_key : "${data['rating_key']}" }, data: { rating_key : "${data['rating_key']}" },
complete: function(xhr, status) { 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> </script>
% endif % endif

View File

@@ -37,13 +37,42 @@ DOCUMENTATION :: END
% else: % else:
<li> <li>
% endif % 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']}"> <a href="info?rating_key=${child['rating_key']}" title="Season ${child['media_index']}">
<div class="item-children-poster"> <div class="item-children-poster">
% if child['thumb']: % 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: % 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 % endif
<div class="item-children-card-overlay"> <div class="item-children-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
@@ -59,7 +88,7 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'episode': % elif data['children_type'] == 'episode':
<a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}"> <a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}">
<div class="item-children-poster"> <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-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
Episode ${child['media_index']} Episode ${child['media_index']}
@@ -79,13 +108,13 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'album': % elif data['children_type'] == 'album':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}"> <a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
</div> </div>
</a> </a>
<div class="item-children-instance-text-wrapper album-item"> <div class="item-children-instance-text-wrapper cover-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3> </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 != None:
% if data['results_count'] > 0: % 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']: % if 'movie' in data['results_list'] and data['results_list']['movie']:
<div class="item-children-wrapper"> <div class="item-children-wrapper">
<div class="item-children-section-title"> <div class="item-children-section-title">
@@ -64,12 +89,12 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
</div> </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 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['year']}</h3> <h3 class="text-muted">${child['year']}</h3>
</div> </div>
@@ -89,12 +114,12 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
</div> </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 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['year']}</h3> <h3 class="text-muted">${child['year']}</h3>
</div> </div>
@@ -114,12 +139,12 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
</div> </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 title="${child['parent_title']}">${child['parent_title']}</h3>
<h3 class="text-muted">S${child['media_index']}</h3> <h3 class="text-muted">S${child['media_index']}</h3>
</div> </div>
@@ -139,7 +164,7 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -165,7 +190,7 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -189,7 +214,7 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -214,7 +239,7 @@ DOCUMENTATION :: END
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster"> <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-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
Track ${child['media_index']} 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>'; 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') { } 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>'; 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>'); $(td).html('<div><div style="float: left;">' + state + '&nbsp;' + date + '</div></div>');
} else if (rowData['group_count'] > 1) { } else if (rowData['group_count'] > 1) {
@@ -203,7 +205,7 @@ history_table_options = {
"targets": [9], "targets": [9],
"data":"stopped", "data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null || rowData['state'] != null) { if (cellData === null || rowData['state'] != "stopped") {
$(td).html('n/a'); $(td).html('n/a');
} else { } else {
$(td).html(moment(cellData,"X").format(time_format)); $(td).html(moment(cellData,"X").format(time_format));

View File

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

View File

@@ -71,11 +71,11 @@ DOCUMENTATION :: END
% endif % endif
</div> </div>
<div class="user-info-nav"> <div class="user-info-nav">
<ul class="user-info-nav"> <ul class="user-info-nav" role="tablist">
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li> <li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#libraryHistory" data-toggle="tab">History</a></li> <li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
% if _session['user_group'] == 'admin': % 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 % endif
</ul> </ul>
</div> </div>
@@ -83,7 +83,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
<div class="tab-content"> <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="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -169,7 +169,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="libraryHistory"> <div role="tabpanel" class="tab-pane" id="tabs-history">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -221,7 +221,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="libraryMediaInfo"> <div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -366,27 +366,8 @@ DOCUMENTATION :: END
<script src="${http_root}js/tables/history_table.js${cache_param}"></script> <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 src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
<script> <script>
$(document).ready(function () { $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$("#edit-library-tooltip").tooltip(); $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
// Populate watch time stats
$.ajax({
url: 'library_watch_time_stats',
async: true,
data: { section_id: section_id },
complete: function(xhr, status) {
$("#library-time-stats").html(xhr.responseText);
}
});
// Populate user stats
$.ajax({
url: 'library_user_stats',
async: true,
data: { section_id: section_id },
complete: function(xhr, status) {
$("#library-user-stats").html(xhr.responseText);
}
}); });
function loadHistoryTable() { function loadHistoryTable() {
@@ -410,14 +391,10 @@ DOCUMENTATION :: END
clearSearchButton('history_table-SID-${data["section_id"]}', history_table); clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
} }
$( "#history-tab-btn" ).one( "click", function() { $('a[href="#tabs-history"]').on('shown.bs.tab', function() {
loadHistoryTable(); 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': % if _session['user_group'] == 'admin':
function loadMediaInfoTable() { function loadMediaInfoTable() {
// Build media info table // Build media info table
@@ -440,7 +417,7 @@ DOCUMENTATION :: END
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table); clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
} }
$( "#media-info-tab-btn" ).one( "click", function() { $('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
loadMediaInfoTable(); loadMediaInfoTable();
}); });
@@ -452,6 +429,8 @@ DOCUMENTATION :: END
refresh_table = false; refresh_table = false;
}); });
$("#edit-library-tooltip").tooltip();
// Load edit library modal // Load edit library modal
$("#toggle-edit-library-modal").click(function() { $("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide'); $("#edit-library-tooltip").tooltip('hide');
@@ -620,6 +599,41 @@ DOCUMENTATION :: END
$("#recently-added-page-right").removeClass("disabled"); $("#recently-added-page-right").removeClass("disabled");
} }
}); });
$(document).ready(function () {
// 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({
url: 'library_watch_time_stats',
async: true,
data: { section_id: section_id },
complete: function(xhr, status) {
$("#library-time-stats").html(xhr.responseText);
}
});
// Populate user stats
$.ajax({
url: 'library_user_stats',
async: true,
data: { section_id: section_id },
complete: function(xhr, status) {
$("#library-user-stats").html(xhr.responseText);
}
});
}); });
</script> </script>
% endif % endif

View File

@@ -3,7 +3,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <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="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
@@ -14,28 +14,21 @@
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
<!-- Favicons --> <!-- 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="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.0"> <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.0"> <link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS --> <!-- ICONS -->
<!-- Android >M39 icon --> <!-- 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.5">
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0"> <meta name="theme-color" content="#282a2d">
<meta name="theme-color" content="#1f1f1f">
<!-- Apple --> <!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0"> <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.0" color="#1f1f1f"> <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-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes"> <!-- Microsoft -->
<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 -->
<meta name="application-name" content="Tautulli"> <meta name="application-name" content="Tautulli">
<meta name="msapplication-TileColor" content="#1f1f1f"> <meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
<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" />
</head> </head>
<body> <body>
@@ -44,7 +37,7 @@
<div class="row"> <div class="row">
<div class="login-container"> <div class="login-container">
<div class="login-logo"> <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>
<div class="row"> <div class="row">
<div class="col-sm-6 col-sm-offset-3"> <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: % elif job in ('Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
<tr> <tr>
<td>${job}</td> <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> <td>N/A</td>
<td>N/A</td> <td>N/A</td>

View File

@@ -18,7 +18,7 @@
</div> </div>
</div> </div>
<div class='table-card-back'> <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>
</div> </div>
</%def> </%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> <p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div> </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> <label for="pms_ip">Plex IP or Hostname</label>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
@@ -569,7 +569,7 @@
<button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button> <button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button>
</span> </span>
</div> </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>
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div> <div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
</div> </div>
@@ -607,7 +607,12 @@
<label for="pms_logs_folder">Plex Web URL</label> <label for="pms_logs_folder">Plex Web URL</label>
<div class="row"> <div class="row">
<div class="col-md-6"> <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>
<div id="pms_web_url_error" class="alert alert-danger settings-alert" role="alert"></div> <div id="pms_web_url_error" class="alert alert-danger settings-alert" role="alert"></div>
</div> </div>
@@ -648,7 +653,7 @@
</div> </div>
<div class="form-group"> <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="row">
<div class="col-md-6"> <div class="col-md-6">
<div class="input-group"> <div class="input-group">
@@ -1110,7 +1115,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;"> <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 /> <br /><br />
Your post may be removed for failure to follow the guidelines. Your post may be removed for failure to follow the guidelines.
</div> </div>
@@ -1131,7 +1137,8 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;"> <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> </div>
<div class="modal-footer"> <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. // Alert the user that their changes require a restart.
function postSaveChecks() { function postSaveChecks() {
if (serverChanged || authChanged || httpChanged || directoryChanged) { if (serverChanged || authChanged || httpChanged || directoryChanged) {
@@ -1609,11 +1627,7 @@ $(document).ready(function() {
} }
$('.save-button').click(function() { $('.save-button').click(function() {
if ($("#pms_identifier").val() == "") { preSaveChecks(function () { saveSettings() });
verifyServer(function () { saveSettings() });
} else {
saveSettings();
}
}); });
initConfigCheckbox('#api_enabled'); initConfigCheckbox('#api_enabled');
@@ -1758,43 +1772,45 @@ $(document).ready(function() {
var pms_identifier = $("#pms_identifier").val(); var pms_identifier = $("#pms_identifier").val();
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0; var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0; var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) { if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>'); $("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
$('#pms-verify').fadeIn('fast');
$.ajax({ $.ajax({
url: 'get_server_id', 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, cache: true,
async: true, async: true,
timeout: 10000, timeout: 10000,
error: function(jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown) {
$("#pms-verify").html('<i class="fa fa-close"></i>'); $("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$('#pms-verify').fadeIn('fast'); $("#pms_ip_group").addClass("has-error");
$("#pms-ip-group").addClass("has-error");
}, },
success: function (json) { success: function (json) {
var machine_identifier = json; var machine_identifier = json;
if (machine_identifier) { if (machine_identifier) {
$("#pms_identifier").val(machine_identifier); $("#pms_identifier").val(machine_identifier);
$("#pms-verify").html('<i class="fa fa-check"></i>'); $("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
$('#pms-verify').fadeIn('fast'); $("#pms_ip_group").removeClass("has-error");
$("#pms-ip-group").removeClass("has-error");
if (_callback) { if (_callback) {
_callback(); _callback();
} }
} else { } else {
$("#pms-verify").html('<i class="fa fa-close"></i>'); $("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$('#pms-verify').fadeIn('fast'); $("#pms_ip_group").addClass("has-error");
$("#pms-ip-group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true) showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true)
} }
} }
}); });
} else { } else {
$("#pms-verify").html('<i class="fa fa-close"></i>'); $("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
$('#pms-verify').fadeIn('fast'); $("#pms_ip_group").addClass("has-error");
$("#pms-ip-group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true) 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(); 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 // Plex.tv auth token fetch
$("#get-pms-auth-token").click(function() { $("#get-pms-auth-token").click(function() {
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Fetching token...'); $("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Fetching token...');
var pms_username = $("#pms_username").val().trim(); var pms_username = $.trim($("#pms_username").val());
var pms_password = $("#pms_password").val().trim(); var pms_password = $.trim($("#pms_password").val());
if ((pms_username !== '') && (pms_password !== '')) { if ((pms_username !== '') && (pms_password !== '')) {
$.ajax({ $.ajax({
type: 'GET', type: 'GET',

View File

@@ -40,7 +40,7 @@
<th align="left" id="delete_row">Delete</th> <th align="left" id="delete_row">Delete</th>
<th align="left" id="state">State</th> <th align="left" id="state">State</th>
<th align="left" id="user">User</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="type">Type</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="device">Device</th> <th align="left" id="device">Device</th>

View File

@@ -61,19 +61,19 @@ DOCUMENTATION :: END
% endif % endif
</div> </div>
<div class="user-info-nav"> <div class="user-info-nav">
<ul class="user-info-nav"> <ul class="user-info-nav" role="tablist">
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li> <li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#userHistory" data-toggle="tab">History</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="#userSyncItems" data-toggle="tab">Synced Items</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="#userAddresses" data-toggle="tab">IP Addresses</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="#userLogins" data-toggle="tab">Tautulli Logins</a></li> <li><a id="login-tab-btn" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
</ul> </ul>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<div class="tab-content"> <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="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -134,7 +134,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="userHistory"> <div role="tabpanel" class="tab-pane" id="tabs-history">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -200,7 +200,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="userSyncItems"> <div role="tabpanel" class="tab-pane" id="tabs-synceditems">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -240,7 +240,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="userAddresses"> <div role="tabpanel" class="tab-pane" id="tabs-ipaddresses">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -271,7 +271,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
<div class="tab-pane" id="userLogins"> <div role="tabpanel" class="tab-pane" id="tabs-tautullilogins">
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -369,14 +369,7 @@ DOCUMENTATION :: END
<script src="${http_root}js/dataTables.bootstrap.min.js"></script> <script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script> <script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
% if data: % if data:
<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> <script>
$(document).ready(function () {
% if str(data['user_id']).isdigit(): % if str(data['user_id']).isdigit():
var user_id = ${data['user_id']}; var user_id = ${data['user_id']};
% else: % else:
@@ -384,27 +377,20 @@ DOCUMENTATION :: END
% endif % endif
var username = '${data['username'].replace("'", "\\'")}'; var username = '${data['username'].replace("'", "\\'")}';
</script>
$("#edit-user-tooltip").tooltip(); <script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
// Populate watch time stats <script src="${http_root}js/tables/user_ips.js${cache_param}"></script>
$.ajax({ <script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
url: 'user_watch_time_stats', <script src="${http_root}js/tables/login_logs.js${cache_param}"></script>
async: true, <script>
data: { user_id: user_id, user: username }, $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
complete: function(xhr, status) { $.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
$("#user-time-stats").html(xhr.responseText);
}
}); });
// Populate platform stats $('a[href="#tabs-profile"]').on('shown.bs.tab', function() {
$.ajax({ var media_type = null;
url: 'user_player_stats', loadHistoryTable(media_type);
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-player-stats").html(xhr.responseText);
}
}); });
function loadHistoryTable(media_type) { function loadHistoryTable(media_type) {
@@ -437,29 +423,12 @@ DOCUMENTATION :: END
}); });
} }
$( "#history-tab-btn" ).one( "click", function() { $('a[href="#tabs-history"]').on('shown.bs.tab', function() {
var media_type = null; var media_type = null;
loadHistoryTable(media_type); loadHistoryTable(media_type);
}); });
$( "#ip-tab-btn" ).one( "click", function() { $('a[href="#tabs-synceditems"]').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);
});
$( "#sync-tab-btn" ).one( "click", function() {
// Build user sync table // Build user sync table
sync_table_options.ajax = { sync_table_options.ajax = {
url: 'get_sync', url: 'get_sync',
@@ -476,7 +445,24 @@ DOCUMENTATION :: END
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table); clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
}); });
$( "#login-tab-btn" ).one( "click", function() { $('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 // Build user login table
login_log_table_options.ajax = { login_log_table_options.ajax = {
url: 'get_user_logins', url: 'get_user_logins',
@@ -493,11 +479,9 @@ DOCUMENTATION :: END
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table); 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': % if _session['user_group'] == 'admin':
$("#edit-user-tooltip").tooltip();
// Load edit user modal // Load edit user modal
$("#toggle-edit-user-modal").click(function() { $("#toggle-edit-user-modal").click(function() {
$("#edit-user-tooltip").tooltip('hide'); $("#edit-user-tooltip").tooltip('hide');
@@ -611,6 +595,41 @@ DOCUMENTATION :: END
$("#recently-watched-page-right").removeClass("disabled"); $("#recently-watched-page-right").removeClass("disabled");
} }
}); });
$(document).ready(function () {
// 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({
url: 'user_watch_time_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-time-stats").html(xhr.responseText);
}
});
// Populate platform stats
$.ajax({
url: 'user_player_stats',
async: true,
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-player-stats").html(xhr.responseText);
}
});
}); });
</script> </script>
% endif % endif

View File

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

View File

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

View File

@@ -278,7 +278,6 @@ def check_server_response():
def check_server_access(): def check_server_access():
with monitor_lock: with monitor_lock:
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
server_response = pms_connect.get_server_response() server_response = pms_connect.get_server_response()

View File

@@ -360,9 +360,9 @@ class ActivityProcessor(object):
'subtitles': session['subtitles'], 'subtitles': session['subtitles'],
'synced_version': session['synced_version'], 'synced_version': session['synced_version'],
'synced_version_profile': session['synced_version_profile'], 'synced_version_profile': session['synced_version_profile'],
'synced_version_title': session['synced_version_title'],
'optimized_version': session['optimized_version'], '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...") # logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_media_info transaction...")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,6 +27,66 @@ import pmsconnect
import session 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(): def update_section_ids():
plexpy.CONFIG.UPDATE_SECTION_IDS = -1 plexpy.CONFIG.UPDATE_SECTION_IDS = -1
@@ -965,7 +1025,7 @@ class Libraries(object):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
# Refresh the PMS_URL to make sure the server_id is updated # 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 server_id = plexpy.CONFIG.PMS_IDENTIFIER

View File

@@ -16,6 +16,7 @@
import arrow import arrow
import bleach import bleach
from collections import Counter
from itertools import groupby from itertools import groupby
import json import json
from operator import itemgetter from operator import itemgetter
@@ -145,9 +146,16 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
return False return False
if notify_action == 'on_concurrent': if notify_action == 'on_concurrent':
ap = activity_processor.ActivityProcessor() pms_connect = pmsconnect.PmsConnect()
user_sessions = ap.get_sessions(user_id=stream_data['user_id'], result = pms_connect.get_current_activity()
ip_address=plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP)
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 return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
elif notify_action == 'on_newdevice': elif notify_action == 'on_newdevice':
@@ -504,7 +512,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
# Build Plex URL # Build Plex URL
metadata['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format( 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, pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
rating_key=rating_key) 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 '') 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'], deviceid_key: self.config['device_id'],
'text': body.encode("utf-8")} 'text': body.encode("utf-8")}
@@ -2783,12 +2783,12 @@ class SCRIPTS(Notifier):
return False return False
if error: 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) logger.error(u"Tautulli Notifiers :: Script error: \n %s" % err)
return False return False
if output: 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) logger.debug(u"Tautulli Notifiers :: Script returned: \n %s" % out)
if not self.script_killed: if not self.script_killed:

View File

@@ -23,7 +23,6 @@ import activity_processor
import database import database
import helpers import helpers
import logger import logger
import plextv
import users 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 # Get the latest friends list so we can pull user id's
try: try:
plextv.refresh_users() users.refresh_users()
except: except:
logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.") logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.")
return None return None

View File

@@ -18,11 +18,9 @@
import base64 import base64
import json import json
from xml.dom import minidom
import plexpy import plexpy
import common import common
import database
import helpers import helpers
import http_handler import http_handler
import logger import logger
@@ -31,129 +29,99 @@ import pmsconnect
import session import session
def refresh_users(): def get_server_resources(return_presence=False):
logger.info(u"Tautulli PlexTV :: Requesting users list refresh...") if not return_presence:
result = PlexTV().get_full_users_list() logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
monitor_db = database.MonitorDatabase() server = {'pms_name': plexpy.CONFIG.PMS_NAME,
user_data = users.Users() 'pms_version': plexpy.CONFIG.PMS_VERSION,
'pms_platform': plexpy.CONFIG.PMS_PLATFORM,
if result: 'pms_ip': plexpy.CONFIG.PMS_IP,
for item in result: 'pms_port': plexpy.CONFIG.PMS_PORT,
'pms_ssl': plexpy.CONFIG.PMS_SSL,
shared_libraries = '' 'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
user_tokens = user_data.get_tokens(user_id=item['user_id']) 'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
if user_tokens and user_tokens['server_token']: 'pms_url': plexpy.CONFIG.PMS_URL,
pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token']) 'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL
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 server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
if item['user_id']: scheme = 'https'
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: else:
new_value_dict['custom_avatar_url'] = item['thumb'] scheme = 'http'
monitor_db.upsert('users', new_value_dict, control_value_dict) fallback_url = '{scheme}://{hostname}:{port}'.format(scheme=scheme,
hostname=server['pms_ip'],
logger.info(u"Tautulli PlexTV :: Users list refreshed.") port=server['pms_port'])
return True
else:
logger.warn(u"Tautulli PlexTV :: Unable to refresh users list.")
return False
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)
plex_tv = PlexTV() plex_tv = PlexTV()
result = plex_tv.get_server_urls(include_https=plexpy.CONFIG.PMS_SSL) result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
plexpass = plex_tv.get_plexpass_status() pms_ip=server['pms_ip'],
pms_port=server['pms_port'],
include_https=server['pms_ssl'])
connections = []
if result: if result:
plexpy.CONFIG.__setattr__('PMS_VERSION', result['version']) connections = result.pop('connections', [])
plexpy.CONFIG.__setattr__('PMS_PLATFORM', result['platform']) server.update(result)
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', plexpass) presence = server.pop('pms_presence', 0)
connections = result['connections'] 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 # 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 connections:
if plexpy.CONFIG.PMS_IS_REMOTE: if server['pms_is_remote']:
# Get all remote connections # 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: else:
# Get all local connections # 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: if conns:
# Get connection with matching address, otherwise return first connection # Get connection with matching address, otherwise return first connection
conn = next((c for c in conns if c['address'] == plexpy.CONFIG.PMS_IP conn = next((c for c in conns if c['address'] == server['pms_ip']
and c['port'] == str(plexpy.CONFIG.PMS_PORT)), conns[0]) and c['port'] == str(server['pms_port'])), conns[0])
plexpy.CONFIG.__setattr__('PMS_URL', conn['uri']) server['pms_url'] = conn['uri']
plexpy.CONFIG.write()
logger.info(u"Tautulli PlexTV :: Server URL retrieved.") logger.info(u"Tautulli PlexTV :: Server URL retrieved.")
# get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL # get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL
if not plexpy.CONFIG.PMS_URL: if not server['pms_url']:
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url) server['pms_url'] = fallback_url
plexpy.CONFIG.write()
logger.warn(u"Tautulli PlexTV :: Unable to retrieve server URLs. Using user-defined value without SSL.") 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: else:
if plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL: server['pms_url'] = fallback_url
fallback_url = fallback_url.replace('http://', 'https://')
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
plexpy.CONFIG.write()
logger.info(u"Tautulli PlexTV :: Using user-defined URL.") logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
plexpy.CONFIG.process_kwargs(server)
plexpy.CONFIG.write()
class PlexTV(object): class PlexTV(object):
""" """
Plex.tv authentication Plex.tv authentication
""" """
def __init__(self, username='', password='', token=None): def __init__(self, username=None, password=None, token=None):
self.protocol = 'HTTPS'
self.username = username self.username = username
self.password = password 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 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 # Check if we should use the admin token, or the guest server token
if session.get_session_user_id(): if session.get_session_user_id():
user_data = users.Users() user_data = users.Users()
@@ -161,12 +129,14 @@ class PlexTV(object):
self.token = user_tokens['server_token'] self.token = user_tokens['server_token']
else: else:
self.token = plexpy.CONFIG.PMS_TOKEN self.token = plexpy.CONFIG.PMS_TOKEN
else:
self.token = token
self.request_handler = http_handler.HTTPHandler(host='plex.tv', if not self.token:
port=443, logger.error(u"Tautulli PlexTV :: PlexTV called, but no token provided.")
return
self.request_handler = http_handler.HTTPHandler(urls=self.urls,
token=self.token, token=self.token,
timeout=self.timeout,
ssl_verify=self.ssl_verify) ssl_verify=self.ssl_verify)
def get_plex_auth(self, output_format='raw'): def get_plex_auth(self, output_format='raw'):
@@ -183,7 +153,6 @@ class PlexTV(object):
} }
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='POST', request_type='POST',
headers=headers, headers=headers,
output_format=output_format, output_format=output_format,
@@ -265,7 +234,6 @@ class PlexTV(object):
def get_plextv_friends(self, output_format=''): def get_plextv_friends(self, output_format=''):
uri = '/api/users' uri = '/api/users'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -274,7 +242,6 @@ class PlexTV(object):
def get_plextv_user_details(self, output_format=''): def get_plextv_user_details(self, output_format=''):
uri = '/users/account' uri = '/users/account'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -283,7 +250,6 @@ class PlexTV(object):
def get_plextv_devices_list(self, output_format=''): def get_plextv_devices_list(self, output_format=''):
uri = '/devices.xml' uri = '/devices.xml'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -292,7 +258,6 @@ class PlexTV(object):
def get_plextv_server_list(self, output_format=''): def get_plextv_server_list(self, output_format=''):
uri = '/pms/servers.xml' uri = '/pms/servers.xml'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -301,7 +266,6 @@ class PlexTV(object):
def get_plextv_sync_lists(self, machine_id='', output_format=''): def get_plextv_sync_lists(self, machine_id='', output_format=''):
uri = '/servers/%s/sync_lists' % machine_id uri = '/servers/%s/sync_lists' % machine_id
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -313,7 +277,6 @@ class PlexTV(object):
else: else:
uri = '/api/resources' uri = '/api/resources'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -325,7 +288,6 @@ class PlexTV(object):
else: else:
uri = '/api/downloads/1.json' uri = '/api/downloads/1.json'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -334,7 +296,6 @@ class PlexTV(object):
def delete_plextv_device(self, device_id='', output_format=''): def delete_plextv_device(self, device_id='', output_format=''):
uri = '/devices/%s.xml' % device_id uri = '/devices/%s.xml' % device_id
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='DELETE', request_type='DELETE',
output_format=output_format) output_format=output_format)
@@ -343,7 +304,6 @@ class PlexTV(object):
def delete_plextv_device_sync_lists(self, client_id='', output_format=''): def delete_plextv_device_sync_lists(self, client_id='', output_format=''):
uri = '/devices/%s/sync_items' % client_id uri = '/devices/%s/sync_items' % client_id
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -352,30 +312,22 @@ class PlexTV(object):
def delete_plextv_sync(self, client_id='', sync_id='', output_format=''): def delete_plextv_sync(self, client_id='', sync_id='', output_format=''):
uri = '/devices/%s/sync_items/%s' % (client_id, sync_id) uri = '/devices/%s/sync_items/%s' % (client_id, sync_id)
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='DELETE', request_type='DELETE',
output_format=output_format) output_format=output_format)
return request return request
def get_full_users_list(self): def get_full_users_list(self):
friends_list = self.get_plextv_friends() friends_list = self.get_plextv_friends(output_format='xml')
own_account = self.get_plextv_user_details() own_account = self.get_plextv_user_details(output_format='xml')
users_list = [] users_list = []
try: try:
xml_parse = minidom.parseString(own_account) xml_head = own_account.getElementsByTagName('user')
except Exception as e: except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account: %s" % e) logger.warn(u"Tautulli PlexTV :: Unable to parse own account XML for get_full_users_list: %s." % e)
return [] return {}
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account.")
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: for a in xml_head:
own_details = {"user_id": helpers.get_xml_attr(a, 'id'), own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'username'), "username": helpers.get_xml_attr(a, 'username'),
@@ -394,18 +346,11 @@ class PlexTV(object):
users_list.append(own_details) users_list.append(own_details)
try: try:
xml_parse = minidom.parseString(friends_list) xml_head = friends_list.getElementsByTagName('User')
except Exception as e: except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list: %s" % e) logger.warn(u"Tautulli PlexTV :: Unable to parse friends list XML for get_full_users_list: %s." % e)
return [] return {}
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list.")
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: for a in xml_head:
friend = {"user_id": helpers.get_xml_attr(a, 'id'), friend = {"user_id": helpers.get_xml_attr(a, 'id'),
"username": helpers.get_xml_attr(a, 'title'), "username": helpers.get_xml_attr(a, 'title'),
@@ -425,34 +370,31 @@ class PlexTV(object):
return users_list return users_list
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None, rating_key_filter=None): def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
sync_list = self.get_plextv_sync_lists(machine_id) 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() user_data = users.Users()
synced_items = [] synced_items = []
try: try:
xml_parse = minidom.parseString(sync_list) xml_head = sync_list.getElementsByTagName('SyncList')
except Exception as e: except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s" % e) logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s." % e)
return [] return {}
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.")
return []
xml_head = xml_parse.getElementsByTagName('SyncList')
if not xml_head:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.")
else:
for a in xml_head: for a in xml_head:
client_id = helpers.get_xml_attr(a, 'clientIdentifier') client_id = helpers.get_xml_attr(a, 'clientIdentifier')
# Filter by client_id # Filter by client_id
if client_id_filter and client_id_filter != client_id: if client_id_filter and str(client_id_filter) != client_id:
continue continue
sync_id = helpers.get_xml_attr(a, 'id') sync_list_id = helpers.get_xml_attr(a, 'id')
sync_device = a.getElementsByTagName('Device') sync_device = a.getElementsByTagName('Device')
for device in sync_device: for device in sync_device:
@@ -473,7 +415,7 @@ class PlexTV(object):
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt') device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
# Filter by user_id # Filter by user_id
if user_id_filter and user_id_filter != device_user_id: if user_id_filter and str(user_id_filter) != device_user_id:
continue continue
for synced in a.getElementsByTagName('SyncItems'): for synced in a.getElementsByTagName('SyncItems'):
@@ -487,10 +429,15 @@ class PlexTV(object):
for idx, item in enumerate(clean_uri) if item == 'metadata'), None) for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
# Filter by rating_key # Filter by rating_key
if rating_key_filter and rating_key_filter != rating_key: if rating_key_filter and str(rating_key_filter) != rating_key:
continue continue
sync_id = helpers.get_xml_attr(item, 'id') sync_id = helpers.get_xml_attr(item, 'id')
# Filter by sync_id
if sync_id_filter and str(sync_id_filter) != sync_id:
continue
sync_version = helpers.get_xml_attr(item, 'version') sync_version = helpers.get_xml_attr(item, 'version')
sync_root_title = helpers.get_xml_attr(item, 'rootTitle') sync_root_title = helpers.get_xml_attr(item, 'rootTitle')
sync_title = helpers.get_xml_attr(item, 'title') sync_title = helpers.get_xml_attr(item, 'title')
@@ -520,11 +467,11 @@ class PlexTV(object):
sync_details = {"device_name": helpers.sanitize(device_name), sync_details = {"device_name": helpers.sanitize(device_name),
"platform": helpers.sanitize(device_platform), "platform": helpers.sanitize(device_platform),
"username": helpers.sanitize(device_username),
"friendly_name": helpers.sanitize(device_friendly_name),
"user_id": device_user_id, "user_id": device_user_id,
"user": helpers.sanitize(device_friendly_name),
"username": helpers.sanitize(device_username),
"root_title": helpers.sanitize(sync_root_title), "root_title": helpers.sanitize(sync_root_title),
"title": helpers.sanitize(sync_title), "sync_title": helpers.sanitize(sync_title),
"metadata_type": sync_metadata_type, "metadata_type": sync_metadata_type,
"content_type": sync_content_type, "content_type": sync_content_type,
"rating_key": rating_key, "rating_key": rating_key,
@@ -550,27 +497,16 @@ class PlexTV(object):
logger.info(u"Tautulli PlexTV :: Deleting sync item '%s'." % sync_id) logger.info(u"Tautulli PlexTV :: Deleting sync item '%s'." % sync_id)
self.delete_plextv_sync(client_id=client_id, sync_id=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: if not pms_identifier:
server_id = plexpy.CONFIG.PMS_IDENTIFIER logger.error(u"Tautulli PlexTV :: Unable to retrieve server connections: no pms_identifier provided.")
else:
logger.error(u"Tautulli PlexTV :: Unable to retrieve server identity.")
return {} 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: try:
xml_parse = minidom.parseString(plextv_resources) 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 {}
except:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls.")
return {}
try:
xml_head = xml_parse.getElementsByTagName('Device')
except Exception as e: except Exception as e:
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s." % e) logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
return {} return {}
@@ -580,16 +516,20 @@ class PlexTV(object):
conn = [] conn = []
connections = device.getElementsByTagName('Connection') connections = device.getElementsByTagName('Connection')
server = {"platform": helpers.get_xml_attr(device, 'platform'), server = {'pms_identifier': helpers.get_xml_attr(device, 'clientIdentifier'),
"version": helpers.get_xml_attr(device, 'productVersion') '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: for c in connections:
server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'), server_details = {'protocol': helpers.get_xml_attr(c, 'protocol'),
"address": helpers.get_xml_attr(c, 'address'), 'address': helpers.get_xml_attr(c, 'address'),
"port": helpers.get_xml_attr(c, 'port'), 'port': helpers.get_xml_attr(c, 'port'),
"uri": helpers.get_xml_attr(c, 'uri'), 'uri': helpers.get_xml_attr(c, 'uri'),
"local": helpers.get_xml_attr(c, 'local') 'local': helpers.get_xml_attr(c, 'local')
} }
conn.append(server_details) conn.append(server_details)
@@ -600,7 +540,7 @@ class PlexTV(object):
# Try to match the device # Try to match the device
for a in xml_head: 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) server = get_connections(a)
break break
@@ -612,15 +552,8 @@ class PlexTV(object):
connections = a.getElementsByTagName('Connection') connections = a.getElementsByTagName('Connection')
for connection in connections: for connection in connections:
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \ if helpers.get_xml_attr(connection, 'address') == pms_ip and \
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT: helpers.get_xml_attr(connection, 'port') == str(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))
server = get_connections(a) server = get_connections(a)
break break
@@ -649,7 +582,7 @@ class PlexTV(object):
return server_times 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 """ """ 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') servers = self.get_plextv_resources(include_https=True, output_format='xml')
clean_servers = [] clean_servers = []
@@ -679,6 +612,7 @@ class PlexTV(object):
connections = d.getElementsByTagName('Connection') connections = d.getElementsByTagName('Connection')
for c in connections: for c in connections:
if not all_servers:
# If this is a remote server don't show any local IPs. # If this is a remote server don't show any local IPs.
if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \ if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
helpers.get_xml_attr(c, 'local') == '1': helpers.get_xml_attr(c, 'local') == '1':
@@ -770,8 +704,6 @@ class PlexTV(object):
return True return True
else: else:
logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.") logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.")
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
plexpy.CONFIG.write()
return False return False
def get_devices_list(self): def get_devices_list(self):

View File

@@ -22,7 +22,6 @@ import activity_processor
import database import database
import helpers import helpers
import logger import logger
import plextv
import users 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 # Get the latest friends list so we can pull user id's
try: try:
plextv.refresh_users() users.refresh_users()
except: except:
logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.") logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.")
return None return None

View File

@@ -13,16 +13,12 @@
# You should have received a copy of the GNU General Public License # 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 Tautulli. If not, see <http://www.gnu.org/licenses/>.
import threading
import urllib import urllib
from urlparse import urlparse
import plexpy import plexpy
import common import common
import database
import helpers import helpers
import http_handler import http_handler
import libraries
import logger import logger
import plextv import plextv
import session import session
@@ -49,83 +45,23 @@ def get_server_friendly_name():
return server_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): class PmsConnect(object):
""" """
Retrieve data from Plex Server Retrieve data from Plex Server
""" """
def __init__(self, token=None): def __init__(self, url=None, token=None):
if plexpy.CONFIG.PMS_URL: self.url = url
url_parsed = urlparse(plexpy.CONFIG.PMS_URL) self.token = token
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'
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 # Check if we should use the admin token, or the guest server token
if session.get_session_user_id(): if session.get_session_user_id():
user_data = users.Users() user_data = users.Users()
@@ -133,12 +69,10 @@ class PmsConnect(object):
self.token = user_tokens['server_token'] self.token = user_tokens['server_token']
else: else:
self.token = plexpy.CONFIG.PMS_TOKEN self.token = plexpy.CONFIG.PMS_TOKEN
else:
self.token = token
self.request_handler = http_handler.HTTPHandler(host=hostname, self.request_handler = http_handler.HTTPHandler(urls=self.url,
port=port, token=self.token,
token=self.token) timeout=self.timeout)
def get_sessions(self, output_format=''): def get_sessions(self, output_format=''):
""" """
@@ -150,7 +84,6 @@ class PmsConnect(object):
""" """
uri = '/status/sessions' uri = '/status/sessions'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -166,7 +99,6 @@ class PmsConnect(object):
""" """
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, reason) uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, reason)
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -183,7 +115,6 @@ class PmsConnect(object):
""" """
uri = '/library/metadata/' + rating_key uri = '/library/metadata/' + rating_key
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -200,7 +131,6 @@ class PmsConnect(object):
""" """
uri = '/library/metadata/' + rating_key + '/children' uri = '/library/metadata/' + rating_key + '/children'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) 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) uri = '/library/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (start, count)
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) 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) 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, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -250,6 +178,22 @@ class PmsConnect(object):
Output: array Output: array
""" """
uri = '/library/metadata/' + rating_key + '/children' 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, request = self.request_handler.make_request(uri=uri,
proto=self.protocol, proto=self.protocol,
request_type='GET', request_type='GET',
@@ -268,7 +212,6 @@ class PmsConnect(object):
""" """
uri = '/library/metadata/' + rating_key + '/allLeaves' uri = '/library/metadata/' + rating_key + '/allLeaves'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -284,7 +227,6 @@ class PmsConnect(object):
""" """
uri = '/servers' uri = '/servers'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -300,7 +242,6 @@ class PmsConnect(object):
""" """
uri = '/:/prefs' uri = '/:/prefs'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -316,7 +257,6 @@ class PmsConnect(object):
""" """
uri = '/identity' uri = '/identity'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -332,7 +272,6 @@ class PmsConnect(object):
""" """
uri = '/library/sections' uri = '/library/sections'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) 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 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, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -367,7 +305,6 @@ class PmsConnect(object):
""" """
uri = '/library/sections/' + section_id + '/label' uri = '/library/sections/' + section_id + '/label'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -384,7 +321,6 @@ class PmsConnect(object):
""" """
uri = '/sync/items/' + sync_id uri = '/sync/items/' + sync_id
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -400,7 +336,6 @@ class PmsConnect(object):
""" """
uri = '/sync/transcodeQueue' uri = '/sync/transcodeQueue'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -414,9 +349,8 @@ class PmsConnect(object):
Output: array 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, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -432,7 +366,6 @@ class PmsConnect(object):
""" """
uri = '/myplex/account' uri = '/myplex/account'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -448,7 +381,6 @@ class PmsConnect(object):
""" """
uri = '/myplex/refreshReachability' uri = '/myplex/refreshReachability'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='PUT') request_type='PUT')
return request return request
@@ -463,7 +395,6 @@ class PmsConnect(object):
""" """
uri = '/updater/check?download=0' uri = '/updater/check?download=0'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='PUT', request_type='PUT',
output_format=output_format) output_format=output_format)
@@ -479,7 +410,6 @@ class PmsConnect(object):
""" """
uri = '/updater/status' uri = '/updater/status'
request = self.request_handler.make_request(uri=uri, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) 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) 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, request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
output_format=output_format) output_format=output_format)
@@ -607,7 +536,7 @@ class PmsConnect(object):
try: try:
xml_head = metadata.getElementsByTagName('MediaContainer') xml_head = metadata.getElementsByTagName('MediaContainer')
except Exception as e: 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 {} return {}
metadata = {} metadata = {}
@@ -1039,6 +968,50 @@ class PmsConnect(object):
helpers.get_xml_attr(metadata_main, 'title')) 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': elif metadata_type == 'clip':
metadata = {'media_type': metadata_type, metadata = {'media_type': metadata_type,
'section_id': section_id, 'section_id': section_id,
@@ -1434,11 +1407,11 @@ class PmsConnect(object):
transcode_decision = 'direct play' transcode_decision = 'direct play'
# Determine if a synced version is being played # Determine if a synced version is being played
sync_id = None
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \ 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': and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
plex_tv = plextv.PlexTV() plex_tv = plextv.PlexTV()
synced_items = plex_tv.get_synced_items(machine_id=plexpy.CONFIG.PMS_IDENTIFIER, synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
client_id_filter=player_details['machine_id'],
rating_key_filter=rating_key) rating_key_filter=rating_key)
if synced_items: if synced_items:
sync_id = synced_items[0]['sync_id'] sync_id = synced_items[0]['sync_id']
@@ -1448,8 +1421,6 @@ class PmsConnect(object):
synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0] synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0]
elif synced_xml_head[0].getElementsByTagName('Video'): elif synced_xml_head[0].getElementsByTagName('Video'):
synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0] synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0]
else:
sync_id = None
# Figure out which version is being played # Figure out which version is being played
if sync_id: if sync_id:
@@ -1796,7 +1767,6 @@ class PmsConnect(object):
else: else:
return False return False
def get_item_children(self, rating_key=''): def get_item_children(self, rating_key=''):
""" """
Return processed and validated children list. Return processed and validated children list.
@@ -1837,8 +1807,11 @@ class PmsConnect(object):
for result in result_data: for result in result_data:
children_output = {'section_id': section_id, children_output = {'section_id': section_id,
'rating_key': helpers.get_xml_attr(result, 'ratingKey'), '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'), 'media_index': helpers.get_xml_attr(result, 'index'),
'title': helpers.get_xml_attr(result, 'title'), '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'), 'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'), 'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration') 'duration': helpers.get_xml_attr(result, 'duration')
@@ -1853,6 +1826,72 @@ class PmsConnect(object):
return output 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): def get_servers_info(self):
""" """
Return the list of local servers. Return the list of local servers.
@@ -2206,7 +2245,6 @@ class PmsConnect(object):
uri = '/photo/:/transcode?%s' % urllib.urlencode(params) uri = '/photo/:/transcode?%s' % urllib.urlencode(params)
result = self.request_handler.make_request(uri=uri, result = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET', request_type='GET',
return_type=True) return_type=True)
@@ -2238,7 +2276,8 @@ class PmsConnect(object):
'episode': [], 'episode': [],
'artist': [], 'artist': [],
'album': [], 'album': [],
'track': [] 'track': [],
'collection': []
} }
for a in xml_head: for a in xml_head:

View File

@@ -23,9 +23,67 @@ import datatables
import helpers import helpers
import logger import logger
import plextv import plextv
import pmsconnect
import session 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): class Users(object):
def __init__(self): 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." logger.warn(u"Tautulli Users :: Unable to retrieve user %s from database. Requesting user list refresh."
% user_id if user_id else user) % 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 # 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) user_details = get_user_details(user_id=user_id, user=user)

View File

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

View File

@@ -175,8 +175,18 @@ def checkGithub(auto_update=False):
if plexpy.COMMITS_BEHIND > 0: if plexpy.COMMITS_BEHIND > 0:
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND) 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 url = 'https://api.github.com/repos/%s/plexpy/releases' % plexpy.CONFIG.GIT_USER
release = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict) 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.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})

View File

@@ -42,6 +42,7 @@ def start_thread():
def on_disconnect(): def on_disconnect():
activity_processor.ActivityProcessor().set_temp_stopped() activity_processor.ActivityProcessor().set_temp_stopped()
plexpy.initialize_scheduler()
def reconnect(): 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.") 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.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
plexpy.PLEX_SERVER_UP = False plexpy.PLEX_SERVER_UP = False
on_disconnect()
plexpy.initialize_scheduler() on_disconnect()
logger.debug(u"Tautulli WebSocket :: Leaving thread.") logger.debug(u"Tautulli WebSocket :: Leaving thread.")

View File

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

View File

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

View File

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