Compare commits
29 Commits
v2.0.2-bet
...
v2.0.5-bet
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9d01335395 | ||
![]() |
63814584e9 | ||
![]() |
bf7e432c0c | ||
![]() |
5a14c229a1 | ||
![]() |
79581fb83e | ||
![]() |
90c587e6e1 | ||
![]() |
b2b728a3cc | ||
![]() |
645ef86c75 | ||
![]() |
450fda18f1 | ||
![]() |
f6162a8df8 | ||
![]() |
a8c5f48b60 | ||
![]() |
edc0b96167 | ||
![]() |
9b4b3e0ecb | ||
![]() |
7bd2c00636 | ||
![]() |
15faccfa2f | ||
![]() |
11e10e7d0e | ||
![]() |
8ef48fc548 | ||
![]() |
29632b0805 | ||
![]() |
e908c45cf2 | ||
![]() |
c37e934f42 | ||
![]() |
41f91956b8 | ||
![]() |
8b2bd5ce79 | ||
![]() |
28e4151157 | ||
![]() |
66d45293e6 | ||
![]() |
243eeeff67 | ||
![]() |
18520e24d1 | ||
![]() |
87743171b7 | ||
![]() |
452f0747c8 | ||
![]() |
a20747044c |
40
CHANGELOG.md
@@ -1,6 +1,44 @@
|
||||
# Changelog
|
||||
|
||||
## v2.0.2-beta (2017-12-XX)
|
||||
## v2.0.5-beta (2017-12-31)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: IPv6 addresses overflowing on the activity cards.
|
||||
* Notifications:
|
||||
* Fix: Error sending Join notifications.
|
||||
* UI:
|
||||
* New: Added total required bandwidth in the activity header.
|
||||
* Fix: Failing to retrieve releases from GitHub.
|
||||
* Other:
|
||||
* Fix: CherryPy SSL connection warning. (Thanks @felixbuenemann)
|
||||
* Fix: Sanitize script output in logs.
|
||||
* Change: Login sessions persists across server restarts.
|
||||
|
||||
|
||||
## v2.0.4-beta (2017-12-29)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Current activity cards duplicating on the homepage.
|
||||
* Notifications:
|
||||
* Fix: Concurrent stream notifications being sent when there is an incorrect number of streams.
|
||||
* UI:
|
||||
* New: Info pages for collections.
|
||||
* New: Button to test Plex Web URL override.
|
||||
* Fix: Library and User pages return to the correct tab when pressing back.
|
||||
|
||||
|
||||
## v2.0.3-beta (2017-12-25)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Missing sync ID error causing logging to crash.
|
||||
* Fix: Incorrect optimized version title column name causing logging to crash.
|
||||
* Notifications:
|
||||
* Fix: Report correct beta version for Tautulli update notifications.
|
||||
* UI:
|
||||
* Fix: Missing CSS for stream info modal.
|
||||
|
||||
|
||||
## v2.0.2-beta (2017-12-24)
|
||||
|
||||
* Monitoring:
|
||||
* Fix: Websocket connection fails to start with existing streams when upgrading to v2.
|
||||
|
@@ -20,26 +20,21 @@
|
||||
${next.headIncludes()}
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.1">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.1">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.1">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android -->
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.1">
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.1">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.1" color="#282a2d">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.1">
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
</head>
|
||||
|
||||
<body class="content">
|
||||
@@ -69,8 +64,8 @@
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
<a class="navbar-brand" href="home">
|
||||
<img alt="Tautulli" src="${http_root}images/logo-tautulli.svg" height="45">
|
||||
<a class="navbar-brand svg" href="home">
|
||||
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 45px;"></object>
|
||||
</a>
|
||||
</div>
|
||||
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
|
||||
|
@@ -74,11 +74,11 @@ DOCUMENTATION :: END
|
||||
<tr>
|
||||
<td class="top-line">Resources:</td>
|
||||
<td class="top-line">
|
||||
<a id="source-link" class="no-highlight" href="${anon_url('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Source</a> |
|
||||
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> |
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Source</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/plexpy/issues' % plexpy.CONFIG.GIT_USER)}" data-id="issue">GitHub Issues</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" data-id="feature request">FeatHub Feature Requests</a> |
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">Tautulli Wiki</a> |
|
||||
<a id="faq-source-link" class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank">Tautulli FAQ</a>
|
||||
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Wiki & FAQ</a> |
|
||||
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" data-id="feature request">FeatHub Feature Requests</a> |
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -112,7 +112,6 @@ DOCUMENTATION :: END
|
||||
|
||||
$('.guidelines-modal-link').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#guidelines-link').attr('href', $('#source-link').attr('href'));
|
||||
$('#guidelines-type').text($(this).data('id'))
|
||||
$('#guidelines-modal').modal();
|
||||
$('#guidelines-continue').attr('href', $(this).attr('href')).on('click', function () {
|
||||
@@ -121,7 +120,6 @@ DOCUMENTATION :: END
|
||||
});
|
||||
$('.support-modal-link').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#faq-link').attr('href', $('#faq-source-link').attr('href'));
|
||||
$('#support-modal').modal();
|
||||
$('#support-continue').attr('href', $(this).attr('href')).on('click', function () {
|
||||
$('#support-modal').modal('hide');
|
||||
|
@@ -13,6 +13,18 @@ a:focus {
|
||||
text-decoration: none;
|
||||
outline: none;
|
||||
}
|
||||
a.svg {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
a.svg:after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
select, .react-selectize.bootstrap3.root-node .react-selectize-control {
|
||||
margin: 5px 0 5px 0;
|
||||
border: 2px solid #444;
|
||||
@@ -841,6 +853,15 @@ a .users-poster-face:hover {
|
||||
-webkit-flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.dashboard-activity-info-item .sub-value .ip-container {
|
||||
display: inline-flex;
|
||||
}
|
||||
.dashboard-activity-info-item .sub-value .ip-address {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 140px;
|
||||
}
|
||||
.dashboard-activity-info-time {
|
||||
position: absolute;
|
||||
top: 200px;
|
||||
@@ -1808,7 +1829,8 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
|
||||
margin-left: 2px;
|
||||
color: #999;
|
||||
}
|
||||
#children-list, #search-results-list {
|
||||
.children-list,
|
||||
.search-results-list {
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
@@ -1874,15 +1896,15 @@ a:hover .item-children-poster {
|
||||
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
z-index: -2;
|
||||
}
|
||||
.item-children-poster-face.season-poster {
|
||||
.item-children-poster-face.poster-item {
|
||||
width: 150px;
|
||||
height: 225px;
|
||||
}
|
||||
.item-children-poster-face.episode-poster {
|
||||
.item-children-poster-face.episode-item {
|
||||
width: 250px;
|
||||
height: 140px;
|
||||
}
|
||||
.item-children-poster-face.album-poster {
|
||||
.item-children-poster-face.cover-item {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
}
|
||||
@@ -1915,15 +1937,13 @@ a:hover .item-children-poster {
|
||||
margin-bottom: 20px;
|
||||
clear: both;
|
||||
}
|
||||
.item-children-instance-text-wrapper.season-item {
|
||||
.item-children-instance-text-wrapper.poster-item,
|
||||
.item-children-instance-text-wrapper.cover-item {
|
||||
width: 150px;
|
||||
}
|
||||
.item-children-instance-text-wrapper.episode-item {
|
||||
width: 250px;
|
||||
}
|
||||
.item-children-instance-text-wrapper.album-item {
|
||||
width: 150px;
|
||||
}
|
||||
.item-children-instance-text-wrapper h3 {
|
||||
width: 100%;
|
||||
padding: 5px 3px 0 3px;
|
||||
@@ -3698,4 +3718,75 @@ a:hover .overlay-refresh-image:hover {
|
||||
}
|
||||
.no-image {
|
||||
background-image: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
#info-modal .stream-info-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
width: 100%;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
#info-modal .stream-info-item .sub-heading {
|
||||
height: 100%;
|
||||
width: 75px;
|
||||
color: #aaa;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
line-height: 14px;
|
||||
-webkit-flex-shrink: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
#info-modal .stream-info-item .sub-value {
|
||||
color: #fff;
|
||||
font-weight: bold;
|
||||
margin-left: 10px;
|
||||
text-align: left;
|
||||
line-height: 14px;
|
||||
-webkit-flex-grow: 1;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.stream-info {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
background-color: #282828;
|
||||
table-layout: fixed;
|
||||
}
|
||||
.stream-info .heading {
|
||||
color: #F9AA03;
|
||||
text-transform: uppercase;
|
||||
font-size: 15px;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
.stream-info th:first-child {
|
||||
width: 125px;
|
||||
height: 30px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.stream-info th:not(:first-child) {
|
||||
text-align: center;
|
||||
font-weight: normal;
|
||||
}
|
||||
.stream-info td {
|
||||
height: 25px;
|
||||
}
|
||||
.stream-info td:first-child {
|
||||
color: #aaa;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.stream-info td:not(:first-child) {
|
||||
text-align: center;
|
||||
}
|
||||
.stream-info tr:nth-child(odd) td {
|
||||
background-color: rgba(255,255,255,0.035);
|
||||
}
|
||||
.stream-info tr:nth-child(even) td {
|
||||
background-color: rgba(255,255,255,0.010);
|
||||
}
|
||||
|
@@ -178,7 +178,7 @@ DOCUMENTATION :: END
|
||||
<ul class="list-unstyled dashboard-activity-info-list">
|
||||
<li class="dashboard-activity-info-item">
|
||||
% if _session['user_group'] == 'admin':
|
||||
<div class="sub-heading"><span class="raw-stream-info-modal" data-toggle="modal" data-target="#raw-stream-info-modal" data-key="${sk}">Stream</span></div>\
|
||||
<div class="sub-heading"><span class="raw-stream-info-modal" data-toggle="modal" data-target="#raw-stream-info-modal" data-key="${sk}">Stream</span></div>
|
||||
% else:
|
||||
<div class="sub-heading">Stream</div>
|
||||
% endif
|
||||
@@ -272,9 +272,9 @@ DOCUMENTATION :: END
|
||||
<div class="sub-heading">Location</div>
|
||||
<div class="sub-value">
|
||||
% if data['ip_address'] != 'N/A':
|
||||
${'LAN' if data['local'] == 1 else 'WAN'}: ${data['ip_address']}
|
||||
${'LAN' if data['local'] == 1 else 'WAN'}: <span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
|
||||
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
|
||||
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
||||
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
|
||||
</a>
|
||||
<script>
|
||||
isPrivateIP("${data['ip_address']}").then(function () {
|
||||
@@ -301,7 +301,7 @@ DOCUMENTATION :: END
|
||||
bw = str(bw) + ' kbps'
|
||||
%>
|
||||
<span id="stream-bandwidth-${sk}">${bw}</span>
|
||||
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate"><i class="fa fa-info-circle"></i></span>
|
||||
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
% elif data['synced_version'] == 1 or data['channel_stream'] == 1:
|
||||
<span id="stream-bandwidth-${sk}">None</span>
|
||||
% else:
|
||||
|
BIN
data/interfaces/default/images/13AD46AC.png
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
data/interfaces/default/images/13AD46AD.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
data/interfaces/default/images/93124B54.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
data/interfaces/default/images/93124B55.png
Normal file
After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -2,9 +2,7 @@
|
||||
<browserconfig>
|
||||
<msapplication>
|
||||
<tile>
|
||||
<square70x70logo src="${http_root}images/favicon/mstile-70x70.png?v=2.0.1"/>
|
||||
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.1"/>
|
||||
<square310x310logo src="${http_root}images/favicon/mstile-310x310.png?v=2.0.1"/>
|
||||
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.5"/>
|
||||
<TileColor>#282a2d</TileColor>
|
||||
</tile>
|
||||
</msapplication>
|
||||
|
Before Width: | Height: | Size: 644 B After Width: | Height: | Size: 553 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 971 B |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
@@ -2,12 +2,12 @@
|
||||
"name": "Tautulli",
|
||||
"icons": [
|
||||
{
|
||||
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.1",
|
||||
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.5",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.1",
|
||||
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.5",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 3.5 KiB |
BIN
data/interfaces/default/images/logo-circle.png
Normal file
After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 22 KiB |
@@ -2,69 +2,72 @@
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="900px"
|
||||
height="300px" viewBox="-41.335 -35.197 900 300" enable-background="new -41.335 -35.197 900 300" xml:space="preserve">
|
||||
height="300px" viewBox="0 0 900 300" enable-background="new 0 0 900 300" xml:space="preserve">
|
||||
<g id="Layer_1">
|
||||
<g id="Logo">
|
||||
|
||||
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="193" xlink:href="13AD46AC.png" transform="matrix(0.9547 0 0 0.9547 -17.8804 54.1814)">
|
||||
</image>
|
||||
|
||||
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="258" xlink:href="13AD46AD.png" transform="matrix(0.9547 0 0 0.9584 -17.8804 -8.8303)">
|
||||
</image>
|
||||
<g>
|
||||
<path fill="none" d="M401.343,203.396c7.493,0,14.836,0,22.407,0c-3.766-12.354-7.495-24.586-11.333-37.176
|
||||
C408.665,178.818,405.017,191.064,401.343,203.396z"/>
|
||||
<path fill="#FFFFFF" d="M436.772,189.627c-4.399-12.521-8.814-25.034-13.224-37.553c-7.552,0-15.105,0-22.657,0.002
|
||||
c-0.14,0.611-0.221,1.246-0.428,1.836c-8.502,24.335-17.017,48.666-25.526,72.996c-1.45,4.146-2.887,8.298-4.362,12.535
|
||||
c6.686,0,13.198,0,19.792,0c2.095-6.873,4.172-13.693,6.248-20.514c10.593,0,21.056,0,31.594,0
|
||||
c2.102,6.894,4.177,13.707,6.245,20.489c6.66,0,13.146,0,19.772,0c-0.229-0.676-0.408-1.213-0.597-1.747
|
||||
C448.014,221.656,442.4,205.639,436.772,189.627z M401.343,203.396c3.674-12.332,7.322-24.578,11.074-37.176
|
||||
c3.837,12.59,7.567,24.82,11.333,37.176C416.179,203.396,408.836,203.396,401.343,203.396z"/>
|
||||
<path fill="#FFFFFF" d="M371.072,152.459c-21.916,0-43.668,0-65.421,0c0,5.111,0,10.227,0,15.341c7.836,0,15.672,0,23.691,0
|
||||
c0,24.046,0,47.786,0,71.621c6.164,0,12.144,0,18.361,0c0-23.901,0-47.698,0-71.682c7.948,0,15.66,0,23.369,0
|
||||
C371.072,162.549,371.072,157.549,371.072,152.459z"/>
|
||||
<path fill="#FFFFFF" d="M532.615,152.555c-6.176,0-12.154,0-18.349,0c0,0.842,0,1.592,0,2.34c0,16.42,0.021,32.838-0.021,49.258
|
||||
c-0.006,2.675-0.119,5.373-0.494,8.018c-0.997,7.07-4.634,11.153-11.06,12.587c-3.731,0.83-7.491,0.754-11.238,0
|
||||
c-5.745-1.157-9.344-4.668-10.58-10.297c-0.678-3.088-0.958-6.317-0.976-9.487c-0.094-16.738-0.045-33.478-0.045-50.217
|
||||
c0-0.732,0-1.467,0-2.148c-6.296,0-12.275,0-18.329,0c0,0.591,0,1.035,0,1.48c0,18.209-0.013,36.416,0.018,54.625
|
||||
c0.003,1.908,0.127,3.835,0.396,5.727c1.632,11.408,7.683,19.563,18.562,23.516c11.146,4.052,22.489,3.83,33.593-0.393
|
||||
c7.937-3.017,13.483-8.627,16.483-16.617c1.474-3.93,2.044-8.024,2.042-12.211c-0.006-18.146-0.002-36.289-0.002-54.436
|
||||
C532.615,153.744,532.615,153.188,532.615,152.555z"/>
|
||||
<path fill="#FFFFFF" d="M688.532,152.555c-6.16,0-12.14,0-18.349,0c0,0.924,0,1.62,0,2.318c0,16.482,0.021,32.967-0.021,49.449
|
||||
c-0.006,2.609-0.125,5.244-0.49,7.824c-1.003,7.127-4.74,11.291-11.226,12.654c-3.674,0.771-7.373,0.702-11.056-0.037
|
||||
c-5.74-1.156-9.354-4.652-10.596-10.281c-0.682-3.086-0.964-6.317-0.979-9.487c-0.094-16.738-0.044-33.478-0.044-50.216
|
||||
c0-0.739,0-1.478,0-2.18c-6.278,0-12.26,0-18.332,0c0,0.582,0,1.024,0,1.467c0.003,18.271-0.011,36.543,0.021,54.815
|
||||
c0.002,1.847,0.132,3.707,0.391,5.533c1.626,11.411,7.669,19.567,18.546,23.533c11.213,4.09,22.621,3.836,33.771-0.445
|
||||
c7.851-3.019,13.345-8.612,16.315-16.541c1.521-4.052,2.06-8.274,2.056-12.592c-0.018-17.953-0.008-35.905-0.008-53.858
|
||||
C688.532,153.893,688.532,153.27,688.532,152.555z"/>
|
||||
<path fill="#FFFFFF" d="M542.405,167.793c7.813,0,15.522,0,23.479,0c0,23.979,0,47.771,0,71.648c6.172,0,12.102,0,18.425,0
|
||||
c0-23.948,0-47.738,0-71.729c7.947,0,15.655,0,23.378,0c0-5.158,0-10.115,0-15.115c-21.819,0-43.53,0-65.281,0
|
||||
C542.405,157.691,542.405,162.648,542.405,167.793z"/>
|
||||
<path fill="#FFFFFF" d="M725.407,152.58c-6.199,0-12.177,0-18.193,0c0,29.021,0,57.92,0,86.857c17.832,0,35.528,0,53.335,0
|
||||
c0-5.191,0-10.146,0-15.307c-11.72,0-23.322,0-35.142,0C725.407,200.143,725.407,176.361,725.407,152.58z"/>
|
||||
<path fill="#FFFFFF" d="M790.668,152.593c-6.197,0-12.172,0-18.216,0c0,28.985,0,57.833,0,86.831c17.905,0,35.6,0,53.371,0
|
||||
c0-5.105,0-10.107,0-15.316c-11.729,0-23.335,0-35.155,0C790.668,200.119,790.668,176.339,790.668,152.593z"/>
|
||||
<path fill="#FFFFFF" d="M838.183,239.464c6.083,0,12.021,0,18.096,0c0-29.09,0-57.987,0-86.879c-6.12,0-12.098,0-18.096,0
|
||||
C838.183,181.611,838.183,210.514,838.183,239.464z"/>
|
||||
<ellipse fill="#E5A00D" cx="847.229" cy="129.416" rx="12.459" ry="12.44"/>
|
||||
</g>
|
||||
</g>
|
||||
<g id="Layer_2">
|
||||
<g id="Layer_2_1_">
|
||||
<g>
|
||||
<path fill="none" d="M349.343,165.396c7.493,0,14.836,0,22.407,0c-3.766-12.354-7.495-24.585-11.333-37.175
|
||||
C356.665,140.819,353.017,153.064,349.343,165.396z"/>
|
||||
<path fill="#FFFFFF" d="M103.83,174.477c-7.6,0-14.441-3.232-19.236-8.394l-4.595,2.845l1.406,1.593
|
||||
c-2.429,3.979-3.83,8.656-3.83,13.66c0,14.5,11.755,26.255,26.255,26.255c10.38,0,19.351-6.024,23.612-14.767l37.675,5.471
|
||||
c0.22,14.31,11.881,25.845,26.244,25.845c14.5,0,26.254-11.755,26.254-26.254c0-14.5-11.754-26.254-26.254-26.254
|
||||
c-10.438,0-19.453,6.092-23.685,14.915l-37.598-5.46c-0.064-6.86-2.759-13.089-7.125-17.73
|
||||
C118.165,171.293,111.371,174.477,103.83,174.477z"/>
|
||||
<path fill="#FFFFFF" d="M77.574,148.22c0-0.054,0.004-0.107,0.004-0.161l-41.163-46.637c3.078-4.301,4.895-9.564,4.895-15.257
|
||||
c0-14.5-11.754-26.254-26.254-26.254c-14.5,0-26.254,11.754-26.254,26.254S0.556,112.42,15.056,112.42
|
||||
c4.464,0,8.666-1.117,12.347-3.083l44.545,50.47l6.701-4.148C77.953,153.299,77.574,150.804,77.574,148.22z"/>
|
||||
<path fill="#E5A00D" d="M191.361,49.885c14.5,0,26.254-11.754,26.254-26.254c0-14.499-11.754-26.254-26.254-26.254
|
||||
s-26.254,11.755-26.254,26.254c0,7.145,2.858,13.619,7.488,18.353l-58.186,82.207c-3.237-1.427-6.814-2.226-10.58-2.226
|
||||
c-14.446,0-26.165,11.669-26.252,26.095c0,0.054-0.004,0.107-0.004,0.161c0,2.584,0.379,5.078,1.075,7.438l-6.701,4.148
|
||||
l-37.506,23.223c-4.801-5.255-11.708-8.552-19.386-8.552c-14.5,0-26.254,11.754-26.254,26.254
|
||||
c0,14.499,11.754,26.254,26.254,26.254c14.5,0,26.254-11.755,26.254-26.254c0-2.506-0.358-4.926-1.014-7.221l39.703-24.582
|
||||
l4.595-2.845c4.794,5.161,11.636,8.394,19.236,8.394c7.542,0,14.335-3.185,19.124-8.275c4.419-4.699,7.132-11.021,7.132-17.981
|
||||
c0-6.396-2.29-12.257-6.092-16.812l58.736-82.983C185.434,49.367,188.336,49.885,191.361,49.885z"/>
|
||||
<path fill="#FFFFFF" d="M384.772,151.627c-4.399-12.521-8.814-25.034-13.223-37.553c-7.552,0-15.106,0-22.657,0.002
|
||||
c-0.14,0.612-0.221,1.246-0.428,1.836c-8.502,24.335-17.017,48.666-25.526,72.996c-1.451,4.146-2.887,8.298-4.363,12.536
|
||||
c6.686,0,13.198,0,19.792,0c2.095-6.874,4.172-13.694,6.248-20.514c10.593,0,21.056,0,31.594,0
|
||||
c2.102,6.893,4.177,13.707,6.245,20.489c6.66,0,13.147,0,19.773,0c-0.229-0.676-0.409-1.213-0.597-1.747
|
||||
C396.014,183.656,390.4,167.638,384.772,151.627z M349.343,165.396c3.674-12.332,7.322-24.577,11.074-37.175
|
||||
c3.837,12.59,7.567,24.82,11.333,37.175C364.179,165.396,356.836,165.396,349.343,165.396z"/>
|
||||
<path fill="#FFFFFF" d="M319.072,114.459c-21.916,0-43.668,0-65.421,0c0,5.112,0,10.227,0,15.341c7.836,0,15.672,0,23.691,0
|
||||
c0,24.046,0,47.786,0,71.621c6.164,0,12.144,0,18.361,0c0-23.902,0-47.698,0-71.682c7.948,0,15.66,0,23.369,0
|
||||
C319.072,124.549,319.072,119.549,319.072,114.459z"/>
|
||||
<path fill="#FFFFFF" d="M480.615,114.554c-6.176,0-12.154,0-18.348,0c0,0.843,0,1.593,0,2.341c0,16.42,0.021,32.838-0.021,49.258
|
||||
c-0.006,2.674-0.119,5.373-0.494,8.017c-0.997,7.07-4.634,11.153-11.06,12.587c-3.731,0.83-7.491,0.754-11.238,0
|
||||
c-5.745-1.157-9.344-4.668-10.58-10.297c-0.678-3.088-0.958-6.318-0.976-9.488c-0.093-16.737-0.044-33.477-0.044-50.216
|
||||
c0-0.733,0-1.467,0-2.149c-6.296,0-12.276,0-18.33,0c0,0.591,0,1.036,0,1.481c0,18.209-0.012,36.416,0.018,54.625
|
||||
c0.003,1.908,0.127,3.835,0.396,5.726c1.632,11.409,7.683,19.563,18.562,23.516c11.146,4.052,22.489,3.83,33.593-0.392
|
||||
c7.936-3.017,13.483-8.627,16.483-16.618c1.474-3.93,2.044-8.024,2.042-12.21c-0.006-18.146-0.002-36.29-0.002-54.436
|
||||
C480.615,115.744,480.615,115.187,480.615,114.554z"/>
|
||||
<path fill="#FFFFFF" d="M636.532,114.554c-6.16,0-12.139,0-18.348,0c0,0.925,0,1.621,0,2.319c0,16.482,0.021,32.967-0.021,49.449
|
||||
c-0.006,2.609-0.125,5.244-0.49,7.825c-1.003,7.126-4.74,11.291-11.226,12.654c-3.674,0.771-7.373,0.702-11.055-0.038
|
||||
c-5.741-1.155-9.354-4.651-10.596-10.281c-0.682-3.086-0.964-6.317-0.979-9.487c-0.093-16.738-0.043-33.477-0.043-50.216
|
||||
c0-0.739,0-1.477,0-2.18c-6.279,0-12.26,0-18.332,0c0,0.582,0,1.025,0,1.467c0.002,18.272-0.011,36.544,0.02,54.816
|
||||
c0.002,1.846,0.132,3.707,0.391,5.533c1.626,11.411,7.669,19.567,18.546,23.533c11.212,4.09,22.621,3.836,33.771-0.445
|
||||
c7.851-3.018,13.345-8.613,16.315-16.541c1.521-4.052,2.06-8.275,2.056-12.592c-0.018-17.953-0.008-35.905-0.008-53.858
|
||||
C636.532,115.892,636.532,115.269,636.532,114.554z"/>
|
||||
<path fill="#FFFFFF" d="M490.405,129.793c7.813,0,15.523,0,23.479,0c0,23.979,0,47.771,0,71.649c6.172,0,12.101,0,18.425,0
|
||||
c0-23.949,0-47.739,0-71.729c7.947,0,15.655,0,23.378,0c0-5.158,0-10.115,0-15.116c-21.82,0-43.531,0-65.282,0
|
||||
C490.405,119.691,490.405,124.648,490.405,129.793z"/>
|
||||
<path fill="#FFFFFF" d="M673.407,114.58c-6.199,0-12.176,0-18.193,0c0,29.021,0,57.92,0,86.857c17.832,0,35.528,0,53.335,0
|
||||
c0-5.191,0-10.146,0-15.306c-11.72,0-23.322,0-35.142,0C673.407,162.143,673.407,138.362,673.407,114.58z"/>
|
||||
<path fill="#FFFFFF" d="M738.668,114.593c-6.197,0-12.172,0-18.216,0c0,28.985,0,57.833,0,86.831c17.905,0,35.6,0,53.371,0
|
||||
c0-5.106,0-10.107,0-15.316c-11.729,0-23.335,0-35.155,0C738.668,162.119,738.668,138.339,738.668,114.593z"/>
|
||||
<path fill="#FFFFFF" d="M786.183,201.464c6.083,0,12.02,0,18.095,0c0-29.09,0-57.987,0-86.879c-6.12,0-12.097,0-18.095,0
|
||||
C786.183,143.612,786.183,172.513,786.183,201.464z"/>
|
||||
<ellipse fill="#E5A00D" cx="795.229" cy="91.416" rx="12.458" ry="12.44"/>
|
||||
|
||||
<image overflow="visible" opacity="0.2" width="279" height="216" xlink:href="13AD46AC.png" transform="matrix(1 0 0 1 15.5 73.5)">
|
||||
</image>
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M243.77,212.601c-10.482,0-19.533,6.118-23.783,14.977l-37.755-5.478
|
||||
c-0.133-14.445-11.881-26.114-26.356-26.114c-5.163,0-9.975,1.489-14.041,4.051l-53.649-60.787
|
||||
c3.091-4.319,4.914-9.605,4.914-15.319c0-14.56-11.802-26.362-26.363-26.362c-14.56,0-26.363,11.803-26.363,26.362
|
||||
c0,14.559,11.803,26.361,26.363,26.361c4.484,0,8.704-1.122,12.399-3.096l54.223,61.438c-2.439,3.994-3.846,8.688-3.846,13.712
|
||||
c0,14.56,11.803,26.363,26.364,26.363c10.422,0,19.43-6.05,23.708-14.828l37.832,5.491c0.22,14.369,11.929,25.953,26.354,25.953
|
||||
c14.56,0,26.363-11.804,26.363-26.362S258.33,212.601,243.77,212.601z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<image overflow="visible" opacity="0.2" width="279" height="279" xlink:href="13AD46AD.png" transform="matrix(1 0 0 1 15.5 10.5)">
|
||||
</image>
|
||||
<g>
|
||||
<path fill="#E5A00D" d="M243.77,34.779c-14.56,0-26.363,11.802-26.363,26.362c0,7.17,2.867,13.667,7.512,18.42l-58.424,82.548
|
||||
c-3.25-1.431-6.84-2.23-10.619-2.23c-14.56,0-26.363,11.8-26.363,26.36c0,2.592,0.38,5.094,1.078,7.46L86.2,221.181
|
||||
c-4.82-5.271-11.754-8.58-19.461-8.58c-14.56,0-26.363,11.803-26.363,26.361s11.803,26.362,26.363,26.362
|
||||
c14.561,0,26.363-11.804,26.363-26.362c0-2.518-0.36-4.951-1.019-7.256l44.477-27.534c4.815,5.183,11.686,8.429,19.318,8.429
|
||||
c14.561,0,26.364-11.803,26.364-26.361c0-6.424-2.301-12.31-6.119-16.883l58.973-83.322c2.72,0.948,5.637,1.468,8.677,1.468
|
||||
c14.561,0,26.363-11.802,26.363-26.361C270.135,46.582,258.33,34.779,243.77,34.779z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.7 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 17 KiB |
@@ -3,20 +3,30 @@
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="298.961px" height="300px" viewBox="0 0 298.961 300" enable-background="new 0 0 298.961 300" xml:space="preserve">
|
||||
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="193" xlink:href="93124B54.png" transform="matrix(1 0 0 1 22.6465 89.25)">
|
||||
</image>
|
||||
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.376,6.382-24.809,15.623l-39.383-5.715
|
||||
c-0.139-15.068-12.392-27.242-27.493-27.242c-5.385,0-10.405,1.554-14.646,4.229l-55.963-63.414
|
||||
c3.224-4.505,5.127-10.02,5.127-15.981c0-15.188-12.312-27.5-27.5-27.5s-27.5,12.312-27.5,27.5s12.312,27.5,27.5,27.5
|
||||
c4.677,0,9.079-1.171,12.934-3.23l56.56,64.089c-2.544,4.168-4.012,9.065-4.012,14.307c0,15.188,12.312,27.5,27.5,27.5
|
||||
c10.872,0,20.269-6.311,24.731-15.467l39.464,5.727c0.229,14.99,12.443,27.074,27.489,27.074c15.188,0,27.5-12.313,27.5-27.5
|
||||
S256.998,215.25,241.811,215.25z"/>
|
||||
<image overflow="visible" opacity="0.15" enable-background="new " width="259" height="259" xlink:href="93124B55.png" transform="matrix(1 0 0 1 22.6465 23.25)">
|
||||
</image>
|
||||
<path fill="#E5A00D" d="M241.811,29.75c-15.188,0-27.5,12.312-27.5,27.5c0,7.48,2.99,14.258,7.835,19.216l-60.943,86.113
|
||||
c-3.389-1.493-7.134-2.329-11.076-2.329c-15.188,0-27.5,12.313-27.5,27.5c0,2.704,0.397,5.314,1.125,7.783l-46.306,28.668
|
||||
c-5.028-5.5-12.261-8.951-20.3-8.951c-15.188,0-27.5,12.313-27.5,27.5s12.312,27.5,27.5,27.5s27.5-12.313,27.5-27.5
|
||||
c0-2.627-0.376-5.165-1.064-7.571l46.396-28.723c5.022,5.407,12.189,8.794,20.15,8.794c15.188,0,27.5-12.313,27.5-27.5
|
||||
c0-6.701-2.399-12.84-6.382-17.611l61.515-86.92c2.836,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,27.5-27.5
|
||||
S256.998,29.75,241.811,29.75z"/>
|
||||
<g>
|
||||
|
||||
<image overflow="visible" opacity="0.2" width="289" height="224" xlink:href="93124B54.png" transform="matrix(1 0 0 1 4.9805 70.5)">
|
||||
</image>
|
||||
<g>
|
||||
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.375,6.382-24.809,15.623l-39.383-5.715
|
||||
c-0.139-15.068-12.393-27.242-27.493-27.242c-5.385,0-10.405,1.554-14.646,4.229l-55.963-63.414
|
||||
c3.224-4.505,5.127-10.02,5.127-15.981c0-15.188-12.312-27.5-27.5-27.5s-27.5,12.312-27.5,27.5s12.312,27.5,27.5,27.5
|
||||
c4.677,0,9.079-1.171,12.934-3.23l56.56,64.089c-2.544,4.168-4.012,9.064-4.012,14.307c0,15.188,12.312,27.5,27.5,27.5
|
||||
c10.872,0,20.269-6.311,24.731-15.467l39.463,5.727c0.229,14.99,12.443,27.074,27.49,27.074c15.188,0,27.5-12.313,27.5-27.5
|
||||
S256.998,215.25,241.811,215.25z"/>
|
||||
</g>
|
||||
</g>
|
||||
<g>
|
||||
|
||||
<image overflow="visible" opacity="0.2" width="289" height="289" xlink:href="93124B55.png" transform="matrix(1 0 0 1 4.9805 5.5)">
|
||||
</image>
|
||||
<g>
|
||||
<path fill="#E5A00D" d="M241.811,29.75c-15.188,0-27.5,12.312-27.5,27.5c0,7.48,2.99,14.258,7.836,19.216l-60.943,86.113
|
||||
c-3.389-1.493-7.135-2.329-11.076-2.329c-15.188,0-27.5,12.313-27.5,27.5c0,2.704,0.397,5.314,1.125,7.783l-46.306,28.668
|
||||
c-5.028-5.5-12.261-8.951-20.3-8.951c-15.188,0-27.5,12.313-27.5,27.5s12.312,27.5,27.5,27.5s27.5-12.313,27.5-27.5
|
||||
c0-2.627-0.376-5.165-1.064-7.57l46.396-28.724c5.022,5.407,12.189,8.794,20.15,8.794c15.188,0,27.5-12.313,27.5-27.5
|
||||
c0-6.701-2.399-12.84-6.382-17.611l61.515-86.92c2.837,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,27.5-27.5
|
||||
S256.998,29.75,241.811,29.75z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 8.9 KiB |
@@ -2,43 +2,43 @@
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="953.503px" height="211.614px" viewBox="0 0 953.503 211.614" enable-background="new 0 0 953.503 211.614"
|
||||
width="956.251px" height="214.137px" viewBox="0 -12.083 956.251 214.137" enable-background="new 0 -12.083 956.251 214.137"
|
||||
xml:space="preserve">
|
||||
<g>
|
||||
<path fill="none" d="M-7.087,418.282c12.933,0,25.607,0,38.673,0c-6.5-21.354-12.936-42.492-19.56-64.253
|
||||
C5.548,375.804-0.747,396.967-7.087,418.282z"/>
|
||||
<path fill="#FFFFFF" d="M54.064,394.482c-7.594-21.64-15.215-43.269-22.824-64.903c-13.035,0-26.072,0-39.106,0.002
|
||||
c-0.241,1.06-0.381,2.153-0.737,3.174c-14.675,42.06-29.37,84.111-44.058,126.164c-2.503,7.166-4.984,14.34-7.53,21.666
|
||||
<path fill="none" d="M165.163,137.282c12.933,0,25.607,0,38.673,0c-6.5-21.354-12.936-42.492-19.56-64.253
|
||||
C177.798,94.804,171.503,115.967,165.163,137.282z"/>
|
||||
<path fill="#FFFFFF" d="M226.314,113.482c-7.594-21.64-15.215-43.269-22.824-64.903c-13.035,0-26.072,0-39.106,0.002
|
||||
c-0.241,1.061-0.381,2.153-0.737,3.174c-14.675,42.061-29.37,84.111-44.058,126.164c-2.503,7.166-4.984,14.34-7.53,21.666
|
||||
c11.54,0,22.78,0,34.162,0c3.614-11.881,7.2-23.67,10.784-35.455c18.283,0,36.34,0,54.529,0
|
||||
c3.627,11.914,7.212,23.691,10.781,35.412c11.493,0,22.693,0,34.128,0c-0.397-1.166-0.705-2.094-1.029-3.02
|
||||
C73.466,449.84,63.776,422.157,54.064,394.482z M-7.087,418.282c6.34-21.314,12.635-42.478,19.113-64.253
|
||||
c6.624,21.761,13.06,42.899,19.56,64.253C18.52,418.282,5.846,418.282-7.087,418.282z"/>
|
||||
<path fill="#FFFFFF" d="M-59.334,330.243c-37.826,0-75.371,0-112.916,0c0,8.836,0,17.674,0.001,26.514c13.525,0,27.05,0,40.889,0
|
||||
c0,41.561,0,82.594,0,123.787c10.64,0,20.961,0,31.692,0c0-41.311,0-82.439,0-123.892c13.718,0,27.028,0,40.334,0
|
||||
C-59.334,347.68-59.334,339.04-59.334,330.243z"/>
|
||||
<path fill="#FFFFFF" d="M219.484,330.409c-10.657,0-20.976,0-31.667,0c0,1.457,0,2.752,0,4.045c0,28.379,0.037,56.758-0.037,85.135
|
||||
C245.716,168.84,236.026,141.157,226.314,113.482z M165.163,137.282c6.34-21.314,12.635-42.478,19.113-64.253
|
||||
c6.624,21.761,13.06,42.899,19.56,64.253C190.77,137.282,178.096,137.282,165.163,137.282z"/>
|
||||
<path fill="#FFFFFF" d="M112.916,49.243c-37.826,0-75.371,0-112.916,0c0,8.836,0,17.674,0.001,26.514c13.525,0,27.05,0,40.889,0
|
||||
c0,41.563,0,82.594,0,123.787c10.64,0,20.961,0,31.692,0c0-41.311,0-82.438,0-123.892c13.718,0,27.028,0,40.334,0
|
||||
C112.916,66.68,112.916,58.04,112.916,49.243z"/>
|
||||
<path fill="#FFFFFF" d="M391.734,49.409c-10.657,0-20.976,0-31.667,0c0,1.457,0,2.752,0,4.045c0,28.379,0.037,56.758-0.037,85.135
|
||||
c-0.012,4.623-0.206,9.287-0.851,13.857c-1.723,12.219-7.998,19.277-19.089,21.752c-6.441,1.436-12.932,1.305-19.396,0.002
|
||||
c-9.917-2-16.129-8.066-18.262-17.797c-1.171-5.336-1.655-10.92-1.685-16.398c-0.158-28.928-0.074-57.86-0.074-86.791
|
||||
c0-1.268,0-2.535,0-3.717c-10.869,0-21.188,0-31.639,0c0,1.023,0,1.793,0,2.562c0,31.47-0.018,62.94,0.032,94.411
|
||||
c0.005,3.299,0.217,6.629,0.683,9.896c2.816,19.719,13.26,33.813,32.036,40.645c19.239,7.002,38.813,6.619,57.978-0.678
|
||||
c-9.917-2-16.129-8.066-18.262-17.797c-1.171-5.336-1.655-10.92-1.685-16.398c-0.158-28.928-0.074-57.859-0.074-86.791
|
||||
c0-1.268,0-2.534,0-3.717c-10.869,0-21.188,0-31.639,0c0,1.023,0,1.793,0,2.563c0,31.471-0.018,62.939,0.032,94.41
|
||||
c0.005,3.301,0.217,6.629,0.683,9.896c2.816,19.719,13.26,33.813,32.036,40.645c19.239,7.002,38.813,6.619,57.978-0.678
|
||||
c13.699-5.215,23.273-14.91,28.45-28.723c2.545-6.791,3.528-13.869,3.526-21.104c-0.011-31.363-0.005-62.723-0.005-94.082
|
||||
C219.484,332.463,219.484,331.502,219.484,330.409z"/>
|
||||
<path fill="#FFFFFF" d="M488.595,333.793c0-1.073,0-2.149,0-3.385c-10.631,0-20.952,0-31.668,0c0,1.598,0,2.803,0,4.008
|
||||
c0,28.488,0.036,56.979-0.037,85.467c-0.011,4.51-0.215,9.064-0.844,13.525c-1.734,12.314-8.183,19.514-19.379,21.869
|
||||
c-6.339,1.332-12.722,1.215-19.076-0.064c-9.912-1.996-16.146-8.039-18.291-17.77c-1.174-5.334-1.662-10.92-1.69-16.398
|
||||
c-0.159-28.928-0.075-57.86-0.075-86.791c0-1.277,0-2.553,0-3.769c-10.835,0-21.158,0-31.641,0c0,1.009,0,1.772,0,2.537
|
||||
c0.003,31.581-0.016,63.161,0.034,94.743c0.005,3.189,0.229,6.406,0.676,9.563c2.806,19.723,13.238,33.82,32.007,40.674
|
||||
c19.354,7.068,39.046,6.631,58.289-0.77c13.549-5.215,23.032-14.887,28.161-28.588c2.623-7.006,3.555-14.305,3.549-21.764
|
||||
C488.578,395.852,488.595,364.823,488.595,333.793z"/>
|
||||
<path fill="#FFFFFF" d="M236.383,356.745c13.484,0,26.794,0,40.523,0c0,41.445,0,82.566,0,123.836c10.655,0,20.887,0,31.803,0
|
||||
c0-41.391,0-82.51-0.001-123.974c13.718,0,27.021,0,40.351,0c0-8.916,0-17.481,0-26.125c-37.661,0-75.134,0-112.676,0
|
||||
C236.383,339.288,236.383,347.853,236.383,356.745z"/>
|
||||
<path fill="#FFFFFF" d="M552.24,330.454c-10.699,0-21.014,0-31.4,0c0,50.158,0,100.104,0,150.119c30.779,0,61.322,0,92.055,0
|
||||
c0-8.973,0-17.535,0-26.453c-20.227,0-40.254,0-60.654,0C552.24,412.659,552.24,371.556,552.24,330.454z"/>
|
||||
<path fill="#FFFFFF" d="M664.878,330.476c-10.697,0-21.01,0-31.441,0c0,50.094,0,99.954,0,150.073c30.904,0,61.445,0,92.119,0
|
||||
c0-8.826,0-17.469,0-26.473c-20.246,0-40.273,0-60.678,0C664.878,412.618,664.878,371.515,664.878,330.476z"/>
|
||||
<path fill="#FFFFFF" d="M746.886,480.62c10.502,0,20.748,0,31.232,0c0-50.277,0-100.226,0-150.158c-10.563,0-20.879,0-31.232,0
|
||||
C746.886,380.631,746.886,430.583,746.886,480.62z"/>
|
||||
<circle fill="#E5A00D" cx="762.501" cy="290.417" r="21.5"/>
|
||||
C391.734,51.463,391.734,50.502,391.734,49.409z"/>
|
||||
<path fill="#FFFFFF" d="M660.845,52.793c0-1.073,0-2.149,0-3.385c-10.631,0-20.953,0-31.668,0c0,1.598,0,2.803,0,4.008
|
||||
c0,28.487,0.035,56.979-0.037,85.467c-0.012,4.51-0.215,9.064-0.844,13.525c-1.734,12.313-8.184,19.514-19.379,21.869
|
||||
c-6.34,1.332-12.723,1.215-19.076-0.064c-9.912-1.996-16.146-8.039-18.291-17.77c-1.174-5.334-1.662-10.92-1.689-16.398
|
||||
c-0.16-28.928-0.076-57.86-0.076-86.791c0-1.277,0-2.553,0-3.77c-10.834,0-21.158,0-31.641,0c0,1.01,0,1.771,0,2.537
|
||||
c0.004,31.581-0.016,63.16,0.033,94.742c0.006,3.189,0.23,6.406,0.678,9.564c2.805,19.723,13.236,33.82,32.006,40.674
|
||||
c19.354,7.068,39.047,6.631,58.289-0.77c13.549-5.217,23.031-14.889,28.16-28.59c2.623-7.006,3.557-14.305,3.551-21.764
|
||||
C660.828,114.852,660.845,83.823,660.845,52.793z"/>
|
||||
<path fill="#FFFFFF" d="M408.633,75.745c13.484,0,26.794,0,40.523,0c0,41.445,0,82.566,0,123.836c10.655,0,20.887,0,31.802,0
|
||||
c0-41.391,0-82.51,0-123.974c13.717,0,27.02,0,40.35,0c0-8.916,0-17.481,0-26.125c-37.66,0-75.133,0-112.675,0
|
||||
C408.633,58.288,408.633,66.853,408.633,75.745z"/>
|
||||
<path fill="#FFFFFF" d="M724.49,49.454c-10.699,0-21.014,0-31.4,0c0,50.158,0,100.104,0,150.119c30.779,0,61.322,0,92.055,0
|
||||
c0-8.973,0-17.535,0-26.453c-20.227,0-40.254,0-60.652,0C724.49,131.659,724.49,90.556,724.49,49.454z"/>
|
||||
<path fill="#FFFFFF" d="M837.128,49.476c-10.697,0-21.01,0-31.441,0c0,50.094,0,99.954,0,150.073c30.904,0,61.445,0,92.119,0
|
||||
c0-8.826,0-17.469,0-26.473c-20.246,0-40.273,0-60.678,0C837.128,131.618,837.128,90.515,837.128,49.476z"/>
|
||||
<path fill="#FFFFFF" d="M919.136,199.62c10.502,0,20.748,0,31.232,0c0-50.277,0-100.226,0-150.158c-10.564,0-20.879,0-31.232,0
|
||||
C919.136,99.631,919.136,149.583,919.136,199.62z"/>
|
||||
<circle fill="#E5A00D" cx="934.751" cy="9.417" r="21.5"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
@@ -11,7 +11,15 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="padded-header" id="current-activity-header">
|
||||
<h3>Activity <small><span id="currentActivityHeader"></span></small></h3>
|
||||
<h3><span id="sessions-shortcut">Activity</span>
|
||||
<small>
|
||||
<span id="currentActivityHeader" style="display: none;">
|
||||
Streams: <span id="currentActivityHeader-streams"></span> |
|
||||
Bandwidth: <span id="currentActivityHeader-bandwidth"></span>
|
||||
<span id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
|
||||
</span>
|
||||
</small>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="currentActivity">
|
||||
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
|
||||
@@ -199,6 +207,7 @@
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
|
||||
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
|
||||
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
|
||||
<script>
|
||||
var date_format = 'YYYY-MM-DD';
|
||||
var time_format = 'hh:mm a';
|
||||
@@ -235,7 +244,14 @@
|
||||
}
|
||||
};
|
||||
|
||||
var create_instances = [];
|
||||
var activity_ready = true;
|
||||
|
||||
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
|
||||
function getCurrentActivity() {
|
||||
activity_ready = false;
|
||||
|
||||
$.ajax({
|
||||
url: 'get_activity',
|
||||
type: 'GET',
|
||||
@@ -261,7 +277,7 @@
|
||||
% else:
|
||||
var msg_settings = ''
|
||||
% endif
|
||||
$('#currentActivityHeader').text('');
|
||||
$('#currentActivityHeader').hide();
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
|
||||
return
|
||||
}
|
||||
@@ -275,20 +291,25 @@
|
||||
// Update the header stream counts
|
||||
var sc_dp = current_activity.stream_count_direct_play,
|
||||
sc_ds = current_activity.stream_count_direct_stream,
|
||||
sc_tc = current_activity.stream_count_transcode;
|
||||
var header_count = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
||||
sc_tc = current_activity.stream_count_transcode,
|
||||
total_bw = current_activity.total_bandwidth;
|
||||
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
|
||||
if (sc_dp) {
|
||||
header_count += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
||||
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
|
||||
}
|
||||
if (sc_ds) {
|
||||
header_count += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', ';
|
||||
streams_header += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', ';
|
||||
}
|
||||
if (sc_tc) {
|
||||
header_count += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', ';
|
||||
streams_header += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', ';
|
||||
}
|
||||
header_count = header_count.replace(/, $/, '');
|
||||
header_count += ')';
|
||||
$('#currentActivityHeader').text(header_count);
|
||||
streams_header = streams_header.replace(/, $/, '') + ')';
|
||||
$('#currentActivityHeader-streams').text(streams_header);
|
||||
|
||||
var bandwidth_header = (total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps');
|
||||
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
|
||||
|
||||
$('#currentActivityHeader').show();
|
||||
|
||||
sessions.forEach(function (session) {
|
||||
var s = new Proxy(session, defaultHandler);
|
||||
@@ -298,6 +319,7 @@
|
||||
|
||||
// Create a new instance if it doesn't exist
|
||||
if (!(instance.length)) {
|
||||
create_instances.push(key);
|
||||
getActivityInstance(key);
|
||||
return;
|
||||
}
|
||||
@@ -498,9 +520,11 @@
|
||||
});
|
||||
|
||||
} else {
|
||||
$('#currentActivityHeader').text('');
|
||||
$('#currentActivityHeader').hide();
|
||||
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>');
|
||||
}
|
||||
|
||||
activity_ready = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -520,13 +544,20 @@
|
||||
$('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 });
|
||||
$('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
|
||||
lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller');
|
||||
|
||||
var index = create_instances.indexOf(session_key);
|
||||
if (index > -1) {
|
||||
create_instances.splice(index, 1);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentActivity();
|
||||
setInterval(function () {
|
||||
getCurrentActivity();
|
||||
if (!(create_instances.length) && activity_ready) {
|
||||
getCurrentActivity();
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
setInterval(function(){
|
||||
@@ -617,6 +648,12 @@
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$('#sessions-shortcut').on('tripleclick', function () {
|
||||
$.getJSON('return_sessions_url', function(sessions_url) {
|
||||
window.open(sessions_url, '_blank');
|
||||
});
|
||||
});
|
||||
% endif
|
||||
</script>
|
||||
% endif
|
||||
|
@@ -80,7 +80,7 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-12">
|
||||
<div class="summary-navbar-list">
|
||||
<ul class="list-unstyled breadcrumb">
|
||||
% if data['media_type'] == 'movie':
|
||||
% if data['media_type'] in ('movie', 'collection'):
|
||||
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
|
||||
<li class="active">${data['title']}</li>
|
||||
% elif data['media_type'] == 'show':
|
||||
@@ -116,9 +116,9 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-9">
|
||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||
% if data['media_type'] == 'track':
|
||||
<a href="${config['pms_web_url'] or 'https://app.plex.tv/desktop'}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
|
||||
% else:
|
||||
<a href="${config['pms_web_url'] or 'https://app.plex.tv/desktop'}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
|
||||
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
|
||||
% endif
|
||||
% if data['media_type'] == 'episode':
|
||||
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);">
|
||||
@@ -151,7 +151,7 @@ DOCUMENTATION :: END
|
||||
</a>
|
||||
</div>
|
||||
<div class="summary-content-title">
|
||||
% if data['media_type'] in ('movie', 'show', 'artist'):
|
||||
% if data['media_type'] in ('movie', 'show', 'artist', 'collection'):
|
||||
<h1> </h1><h1>${data['title']}</h1>
|
||||
% elif data['media_type'] == 'season':
|
||||
<h1> </h1><h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
|
||||
@@ -175,7 +175,7 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-9">
|
||||
% if data['media_type'] == 'movie':
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
|
||||
% elif data['media_type'] == 'show' or data['media_type'] == 'season':
|
||||
% elif data['media_type'] in ('show', 'season', 'collection'):
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
|
||||
% elif data['media_type'] == 'episode':
|
||||
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;">
|
||||
@@ -235,6 +235,8 @@ DOCUMENTATION :: END
|
||||
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
||||
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
|
||||
Released <strong> ${data['year']}</strong>
|
||||
% elif data['media_type'] == 'collection':
|
||||
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-details-tag">
|
||||
@@ -308,51 +310,65 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
% if data['media_type'] == 'show':
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Season List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading season list...</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading season list...</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['media_type'] == 'season':
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Episode List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading episode list...</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading episode list...</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['media_type'] == 'artist':
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Album List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading album list...</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading album list...</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['media_type'] == 'album':
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Track List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<div id="children-list"><i class="fa fa-refresh fa-spin"></i> Loading track list...</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading track list...</div>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
% elif data['media_type'] == 'collection':
|
||||
<div class="col-md-12">
|
||||
<div class='table-card-header'>
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Movies in <strong>${data['title']}</strong> collection</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i> Loading movies list...</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="collection-related-list-container" style="display: none;">
|
||||
</div>
|
||||
% endif
|
||||
% if data['media_type'] != 'collection':
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span>Watch History for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
@@ -420,6 +436,7 @@ DOCUMENTATION :: END
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -562,6 +579,7 @@ DOCUMENTATION :: END
|
||||
}
|
||||
</script>
|
||||
% endif
|
||||
% if data['media_type'] != 'collection':
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
get_history();
|
||||
@@ -638,7 +656,8 @@ DOCUMENTATION :: END
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% if data['media_type'] in ('show', 'season', 'artist', 'album'):
|
||||
% endif
|
||||
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
|
||||
<script>
|
||||
$.ajax({
|
||||
url: 'get_item_children',
|
||||
@@ -646,7 +665,24 @@ DOCUMENTATION :: END
|
||||
async: true,
|
||||
data: { rating_key : "${data['rating_key']}" },
|
||||
complete: function(xhr, status) {
|
||||
$("#children-list").html(xhr.responseText); }
|
||||
$("#children-list").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
% if data['media_type'] == 'collection':
|
||||
<script>
|
||||
$.ajax({
|
||||
url: 'get_item_children_related',
|
||||
type: 'GET',
|
||||
async: true,
|
||||
data: {
|
||||
rating_key : "${data['rating_key']}",
|
||||
title: "${data['title']}"
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#collection-related-list-container").html(xhr.responseText).show();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
@@ -37,13 +37,42 @@ DOCUMENTATION :: END
|
||||
% else:
|
||||
<li>
|
||||
% endif
|
||||
%if data['children_type'] == 'season':
|
||||
% if data['children_type'] == 'movie':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
<h3 class="text-muted">${child['year']}</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'show':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
</div>
|
||||
% elif data['children_type'] == 'season':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="Season ${child['media_index']}">
|
||||
<div class="item-children-poster">
|
||||
% if child['thumb']:
|
||||
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
|
||||
% else:
|
||||
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
|
||||
% endif
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
@@ -59,7 +88,7 @@ DOCUMENTATION :: END
|
||||
% elif data['children_type'] == 'episode':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
|
||||
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
Episode ${child['media_index']}
|
||||
@@ -79,13 +108,13 @@ DOCUMENTATION :: END
|
||||
% elif data['children_type'] == 'album':
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
</a>
|
||||
<div class="item-children-instance-text-wrapper album-item">
|
||||
<div class="item-children-instance-text-wrapper cover-item">
|
||||
<h3>
|
||||
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
|
||||
</h3>
|
||||
|
99
data/interfaces/default/info_collection_list.html
Normal 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
|
@@ -54,6 +54,31 @@ DOCUMENTATION :: END
|
||||
|
||||
% if data != None:
|
||||
% if data['results_count'] > 0:
|
||||
% if 'collection' in data['results_list'] and data['results_list']['collection']:
|
||||
<div class="item-children-wrapper">
|
||||
<div class="item-children-section-title">
|
||||
<h4>Collections</h4>
|
||||
</div>
|
||||
<ul class="item-children-instance list-unstyled">
|
||||
% for child in data['results_list']['collection']:
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
<h3 class="text-muted">${child['min_year']} - ${child['max_year']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
% if 'movie' in data['results_list'] and data['results_list']['movie']:
|
||||
<div class="item-children-wrapper">
|
||||
<div class="item-children-section-title">
|
||||
@@ -64,12 +89,12 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="item-children-instance-text-wrapper season-item">
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
<h3 class="text-muted">${child['year']}</h3>
|
||||
</div>
|
||||
@@ -89,12 +114,12 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="item-children-instance-text-wrapper season-item">
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3 title="${child['title']}">${child['title']}</h3>
|
||||
<h3 class="text-muted">${child['year']}</h3>
|
||||
</div>
|
||||
@@ -114,12 +139,12 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
</div>
|
||||
<div class="item-children-instance-text-wrapper season-item">
|
||||
<div class="item-children-instance-text-wrapper poster-item">
|
||||
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
|
||||
<h3 class="text-muted">S${child['media_index']}</h3>
|
||||
</div>
|
||||
@@ -139,7 +164,7 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
|
||||
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -165,7 +190,7 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -189,7 +214,7 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
|
||||
% endif
|
||||
@@ -214,7 +239,7 @@ DOCUMENTATION :: END
|
||||
<li>
|
||||
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
|
||||
<div class="item-children-poster">
|
||||
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
|
||||
<div class="item-children-card-overlay">
|
||||
<div class="item-children-overlay-text">
|
||||
Track ${child['media_index']}
|
||||
|
1
data/interfaces/default/js/jquery.tripleclick.min.js
vendored
Normal 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);
|
@@ -59,6 +59,8 @@ history_table_options = {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-pause fa-fw"></i></span>';
|
||||
} else if (rowData['state'] === 'buffering') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-spinner fa-fw"></i></span>';
|
||||
} else if (rowData['state'] === 'stopped') {
|
||||
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
|
||||
}
|
||||
$(td).html('<div><div style="float: left;">' + state + ' ' + date + '</div></div>');
|
||||
} else if (rowData['group_count'] > 1) {
|
||||
@@ -203,7 +205,7 @@ history_table_options = {
|
||||
"targets": [9],
|
||||
"data":"stopped",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData === null || rowData['state'] != null) {
|
||||
if (cellData === null || rowData['state'] != "stopped") {
|
||||
$(td).html('n/a');
|
||||
} else {
|
||||
$(td).html(moment(cellData,"X").format(time_format));
|
||||
|
@@ -47,7 +47,7 @@ sync_table_options = {
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"data": "friendly_name",
|
||||
"data": "user",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['user_id']) {
|
||||
@@ -63,7 +63,7 @@ sync_table_options = {
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"data": "title",
|
||||
"data": "sync_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['metadata_type'] !== '') {
|
||||
|
@@ -71,11 +71,11 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="user-info-nav">
|
||||
<ul class="user-info-nav">
|
||||
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#libraryHistory" data-toggle="tab">History</a></li>
|
||||
<ul class="user-info-nav" role="tablist">
|
||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
% if _session['user_group'] == 'admin':
|
||||
<li><a id="media-info-tab-btn" href="#libraryMediaInfo" data-toggle="tab">Media Info</a></li>
|
||||
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
|
||||
% endif
|
||||
</ul>
|
||||
</div>
|
||||
@@ -83,7 +83,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="profile">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-profile">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -169,7 +169,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="libraryHistory">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-history">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -221,7 +221,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="libraryMediaInfo">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -366,8 +366,253 @@ DOCUMENTATION :: END
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
|
||||
<script>
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
function loadHistoryTable() {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
||||
};
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
|
||||
}
|
||||
|
||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
||||
loadHistoryTable();
|
||||
});
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
function loadMediaInfoTable() {
|
||||
// Build media info table
|
||||
media_info_table_options.ajax = {
|
||||
url: 'get_library_media_info',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
refresh: refresh_table
|
||||
};
|
||||
}
|
||||
}
|
||||
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-media-info');
|
||||
|
||||
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
|
||||
}
|
||||
|
||||
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
|
||||
loadMediaInfoTable();
|
||||
});
|
||||
|
||||
$("#refresh-media-info-table").click(function () {
|
||||
media_info_child_table = {};
|
||||
refresh_table = true;
|
||||
refresh_child_tables = true;
|
||||
media_info_table.draw();
|
||||
refresh_table = false;
|
||||
});
|
||||
|
||||
$("#edit-library-tooltip").tooltip();
|
||||
|
||||
// Load edit library modal
|
||||
$("#toggle-edit-library-modal").click(function() {
|
||||
$("#edit-library-tooltip").tooltip('hide');
|
||||
$.ajax({
|
||||
url: 'edit_library_dialog',
|
||||
data: { section_id: section_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#edit-library-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
% endif
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'library_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
section_id: section_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#library-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recentlyAdded() {
|
||||
// Populate recently added
|
||||
$.ajax({
|
||||
url: 'library_recently_added',
|
||||
async: true,
|
||||
data: {
|
||||
section_id: section_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#library-recently-added").html(xhr.responseText);
|
||||
highlightAddedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
recentlyAdded();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#library-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
function highlightAddedScrollerButton() {
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#library-recently-added").width()) {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-added-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
highlightWatchedScrollerButton();
|
||||
highlightAddedScrollerButton();
|
||||
});
|
||||
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
|
||||
var leftTotalWatched = 0;
|
||||
$(".paginate-watched").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#library-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotalWatched }, 250);
|
||||
|
||||
if (leftTotalWatched == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotalWatched == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
var leftTotalAdded = 0;
|
||||
$(".paginate-added").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var containerWidth = $("#library-recently-added").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotalAdded }, 250);
|
||||
|
||||
if (leftTotalAdded == 0) {
|
||||
$("#recently-added-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotalAdded == leftMax) {
|
||||
$("#recently-added-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
$("#edit-library-tooltip").tooltip();
|
||||
|
||||
// Javascript to enable link to tab
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
if (hash) {
|
||||
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
|
||||
}
|
||||
|
||||
// Change hash for page-reload
|
||||
$('.user-info-nav a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||
});
|
||||
|
||||
// Populate watch time stats
|
||||
$.ajax({
|
||||
@@ -389,237 +634,6 @@ DOCUMENTATION :: END
|
||||
}
|
||||
});
|
||||
|
||||
function loadHistoryTable() {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
|
||||
};
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
|
||||
}
|
||||
|
||||
$( "#history-tab-btn" ).one( "click", function() {
|
||||
loadHistoryTable();
|
||||
});
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
function loadMediaInfoTable() {
|
||||
// Build media info table
|
||||
media_info_table_options.ajax = {
|
||||
url: 'get_library_media_info',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
section_id: section_id,
|
||||
refresh: refresh_table
|
||||
};
|
||||
}
|
||||
}
|
||||
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
|
||||
$(colvis.button()).appendTo('#button-bar-media-info');
|
||||
|
||||
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
|
||||
}
|
||||
|
||||
$( "#media-info-tab-btn" ).one( "click", function() {
|
||||
loadMediaInfoTable();
|
||||
});
|
||||
|
||||
$("#refresh-media-info-table").click(function () {
|
||||
media_info_child_table = {};
|
||||
refresh_table = true;
|
||||
refresh_child_tables = true;
|
||||
media_info_table.draw();
|
||||
refresh_table = false;
|
||||
});
|
||||
|
||||
// Load edit library modal
|
||||
$("#toggle-edit-library-modal").click(function() {
|
||||
$("#edit-library-tooltip").tooltip('hide');
|
||||
$.ajax({
|
||||
url: 'edit_library_dialog',
|
||||
data: { section_id: section_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#edit-library-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
% endif
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'library_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
section_id: section_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#library-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function recentlyAdded() {
|
||||
// Populate recently added
|
||||
$.ajax({
|
||||
url: 'library_recently_added',
|
||||
async: true,
|
||||
data: {
|
||||
section_id: section_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#library-recently-added").html(xhr.responseText);
|
||||
highlightAddedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
recentlyAdded();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#library-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
function highlightAddedScrollerButton() {
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#library-recently-added").width()) {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-added-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
highlightWatchedScrollerButton();
|
||||
highlightAddedScrollerButton();
|
||||
});
|
||||
|
||||
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
|
||||
|
||||
var leftTotalWatched = 0;
|
||||
$(".paginate-watched").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#library-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotalWatched }, 250);
|
||||
|
||||
if (leftTotalWatched == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotalWatched == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
var leftTotalAdded = 0;
|
||||
$(".paginate-added").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-added-row-scroller");
|
||||
var containerWidth = $("#library-recently-added").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotalAdded }, 250);
|
||||
|
||||
if (leftTotalAdded == 0) {
|
||||
$("#recently-added-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotalAdded == leftMax) {
|
||||
$("#recently-added-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-added-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
@@ -3,7 +3,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Tautulli - ${title} | ${server_name}</title>
|
||||
<title>Tautulli - ${title}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="description" content="">
|
||||
<meta name="author" content="">
|
||||
@@ -14,28 +14,21 @@
|
||||
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
|
||||
|
||||
<!-- Favicons -->
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.0">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.0">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.0">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
|
||||
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
|
||||
|
||||
<!-- ICONS -->
|
||||
<!-- Android >M39 icon -->
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
|
||||
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0">
|
||||
<meta name="theme-color" content="#1f1f1f">
|
||||
<!-- Android -->
|
||||
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
|
||||
<meta name="theme-color" content="#282a2d">
|
||||
<!-- Apple -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.0" color="#1f1f1f">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
|
||||
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
|
||||
<meta name="apple-mobile-web-app-title" content="Tautulli">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
<meta name="viewport" content="initial-scale=1">
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<!-- IE10 icon -->
|
||||
<!-- Microsoft -->
|
||||
<meta name="application-name" content="Tautulli">
|
||||
<meta name="msapplication-TileColor" content="#1f1f1f">
|
||||
<meta name="msapplication-TileImage" content="${http_root}images/favicon/mstile-144x144.png?v=2.0.0">
|
||||
<meta name="msapplication-config" content="${http_root}xml/IEconfig.xml?v=2.0.0" />
|
||||
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -44,7 +37,7 @@
|
||||
<div class="row">
|
||||
<div class="login-container">
|
||||
<div class="login-logo">
|
||||
<img alt="Tautulli" src="${http_root}images/logo-tautulli.svg" height="100">
|
||||
<object data="${http_root}images/logo-tautulli.svg" type="image/svg+xml" style="height: 100px;"></object>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
|
@@ -45,7 +45,7 @@ DOCUMENTATION :: END
|
||||
% elif job in ('Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED:
|
||||
<tr>
|
||||
<td>${job}</td>
|
||||
<td><i class="fa fa-sm fa-fw fa-check"></i> Using Websocket</td>
|
||||
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
|
@@ -18,7 +18,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<div id="search-results-list"><i class="fa fa-refresh fa-spin"></i> Loading search results...</div>
|
||||
<div id="search-results-list" class="search-results-list"><i class="fa fa-refresh fa-spin"></i> Loading search results...</div>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
@@ -559,7 +559,7 @@
|
||||
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group has-feedback" id="pms-ip-group">
|
||||
<div class="form-group has-feedback" id="pms_ip_group">
|
||||
<label for="pms_ip">Plex IP or Hostname</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
@@ -569,7 +569,7 @@
|
||||
<button class="btn btn-form" type="button" id="verify_server_button">Verify Server</button>
|
||||
</span>
|
||||
</div>
|
||||
<span class="form-control-feedback" id="pms-verify" aria-hidden="true" style="display: none; right: 110px;"></span>
|
||||
<span class="form-control-feedback" id="pms_verify" aria-hidden="true" style="display: none; right: 110px;"></span>
|
||||
</div>
|
||||
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
@@ -607,7 +607,12 @@
|
||||
<label for="pms_logs_folder">Plex Web URL</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/.+\/web\/index\.html$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL." required>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="pms_web_url" name="pms_web_url" value="${config['pms_web_url']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^https?:\/\/\S+$|^https:\/\/app.plex.tv\/desktop$" data-parsley-errors-container="#pms_web_url_error" data-parsley-error-message="Invalid Plex Web URL.">
|
||||
<span class="input-group-btn">
|
||||
<button class="btn btn-form" type="button" id="test_pms_web_button">Test URL</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="pms_web_url_error" class="alert alert-danger settings-alert" role="alert"></div>
|
||||
</div>
|
||||
@@ -648,7 +653,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="pms_token">PMS Token</label>
|
||||
<label for="pms_token">Plex.tv Account Token</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="input-group">
|
||||
@@ -1110,7 +1115,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
|
||||
<strong>Please read the <a href="#" target="_blank" id="guidelines-link">guidelines</a> in the README document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
|
||||
<strong>Please read the <a href="${anon_url('https://github.com/%s/plexpy/blob/master/CONTRIBUTING.md' % plexpy.CONFIG.GIT_USER)}" target="_blank">guidelines</a>
|
||||
in the CONTRIBUTING document <br />before submitting a new <span id="guidelines-type"></span>!</strong>
|
||||
<br /><br />
|
||||
Your post may be removed for failure to follow the guidelines.
|
||||
</div>
|
||||
@@ -1131,7 +1137,8 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div style="text-align: center; margin-top: 20px; margin-bottom: 20px;">
|
||||
<strong>Please read the <a href="#" target="_blank" id="faq-link">FAQ</a> before asking for help!</strong>
|
||||
<strong>Please read the <a href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank">FAQ</a>
|
||||
before asking for help!</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -1580,6 +1587,17 @@ $(document).ready(function() {
|
||||
}
|
||||
}
|
||||
|
||||
function preSaveChecks(_callback) {
|
||||
if ($("#pms_identifier").val() == "") {
|
||||
verifyServer();
|
||||
}
|
||||
verifyPMSWebURL();
|
||||
|
||||
if (_callback) {
|
||||
_callback();
|
||||
}
|
||||
}
|
||||
|
||||
// Alert the user that their changes require a restart.
|
||||
function postSaveChecks() {
|
||||
if (serverChanged || authChanged || httpChanged || directoryChanged) {
|
||||
@@ -1609,11 +1627,7 @@ $(document).ready(function() {
|
||||
}
|
||||
|
||||
$('.save-button').click(function() {
|
||||
if ($("#pms_identifier").val() == "") {
|
||||
verifyServer(function () { saveSettings() });
|
||||
} else {
|
||||
saveSettings();
|
||||
}
|
||||
preSaveChecks(function () { saveSettings() });
|
||||
});
|
||||
|
||||
initConfigCheckbox('#api_enabled');
|
||||
@@ -1758,43 +1772,45 @@ $(document).ready(function() {
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||
|
||||
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
|
||||
$("#pms-verify").html('<i class="fa fa-refresh fa-spin"></i>');
|
||||
$('#pms-verify').fadeIn('fast');
|
||||
$("#pms_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
|
||||
$.ajax({
|
||||
url: 'get_server_id',
|
||||
data : { hostname: pms_ip, port: pms_port, identifier: pms_identifier, ssl: pms_ssl, remote: pms_is_remote },
|
||||
data: {
|
||||
hostname: pms_ip,
|
||||
port: pms_port,
|
||||
identifier: pms_identifier,
|
||||
ssl: pms_ssl,
|
||||
remote: pms_is_remote
|
||||
},
|
||||
cache: true,
|
||||
async: true,
|
||||
timeout: 10000,
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$("#pms-verify").html('<i class="fa fa-close"></i>');
|
||||
$('#pms-verify').fadeIn('fast');
|
||||
$("#pms-ip-group").addClass("has-error");
|
||||
error: function (jqXHR, textStatus, errorThrown) {
|
||||
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").addClass("has-error");
|
||||
},
|
||||
success: function (json) {
|
||||
var machine_identifier = json;
|
||||
if (machine_identifier) {
|
||||
$("#pms_identifier").val(machine_identifier);
|
||||
$("#pms-verify").html('<i class="fa fa-check"></i>');
|
||||
$('#pms-verify').fadeIn('fast');
|
||||
$("#pms-ip-group").removeClass("has-error");
|
||||
$("#pms_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").removeClass("has-error");
|
||||
|
||||
if (_callback) {
|
||||
_callback();
|
||||
}
|
||||
} else {
|
||||
$("#pms-verify").html('<i class="fa fa-close"></i>');
|
||||
$('#pms-verify').fadeIn('fast');
|
||||
$("#pms-ip-group").addClass("has-error");
|
||||
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").addClass("has-error");
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true)
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
$("#pms-verify").html('<i class="fa fa-close"></i>');
|
||||
$('#pms-verify').fadeIn('fast');
|
||||
$("#pms-ip-group").addClass("has-error");
|
||||
$("#pms_verify").html('<i class="fa fa-close"></i>').fadeIn('fast');
|
||||
$("#pms_ip_group").addClass("has-error");
|
||||
showMsg('<i class="fa fa-exclamation-circle"></i> Could not verify your server.', false, true, 5000, true)
|
||||
}
|
||||
}
|
||||
@@ -1804,11 +1820,21 @@ $(document).ready(function() {
|
||||
verifyServer();
|
||||
});
|
||||
|
||||
function verifyPMSWebURL() {
|
||||
var pms_web_url = $.trim($("#pms_web_url").val());
|
||||
$("#pms_web_url").val(pms_web_url || 'https://app.plex.tv/desktop');
|
||||
}
|
||||
|
||||
$('#test_pms_web_button').on('click', function(){
|
||||
var pms_web_url = $.trim($("#pms_web_url").val());
|
||||
window.open(pms_web_url, '_blank');
|
||||
});
|
||||
|
||||
// Plex.tv auth token fetch
|
||||
$("#get-pms-auth-token").click(function() {
|
||||
$("#pms-token-status").html('<i class="fa fa-refresh fa-spin"></i> Fetching token...');
|
||||
var pms_username = $("#pms_username").val().trim();
|
||||
var pms_password = $("#pms_password").val().trim();
|
||||
var pms_username = $.trim($("#pms_username").val());
|
||||
var pms_password = $.trim($("#pms_password").val());
|
||||
if ((pms_username !== '') && (pms_password !== '')) {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
|
@@ -40,7 +40,7 @@
|
||||
<th align="left" id="delete_row">Delete</th>
|
||||
<th align="left" id="state">State</th>
|
||||
<th align="left" id="user">User</th>
|
||||
<th align="left" id="title">Title</th>
|
||||
<th align="left" id="sync_title">Title</th>
|
||||
<th align="left" id="type">Type</th>
|
||||
<th align="left" id="platform">Platform</th>
|
||||
<th align="left" id="device">Device</th>
|
||||
|
@@ -61,19 +61,19 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</div>
|
||||
<div class="user-info-nav">
|
||||
<ul class="user-info-nav">
|
||||
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#userHistory" data-toggle="tab">History</a></li>
|
||||
<li><a id="sync-tab-btn" href="#userSyncItems" data-toggle="tab">Synced Items</a></li>
|
||||
<li><a id="ip-tab-btn" href="#userAddresses" data-toggle="tab">IP Addresses</a></li>
|
||||
<li><a id="login-tab-btn" href="#userLogins" data-toggle="tab">Tautulli Logins</a></li>
|
||||
<ul class="user-info-nav" role="tablist">
|
||||
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
|
||||
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
|
||||
<li><a id="sync-tab-btn" href="#tabs-synceditems" role="tab" data-toggle="tab">Synced Items</a></li>
|
||||
<li><a id="ip-tab-btn" href="#tabs-ipaddresses" role="tab" data-toggle="tab">IP Addresses</a></li>
|
||||
<li><a id="login-tab-btn" href="#tabs-tautullilogins" role="tab" data-toggle="tab">Tautulli Logins</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane active" id="profile">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-profile">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -134,7 +134,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="userHistory">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-history">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -200,7 +200,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="userSyncItems">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-synceditems">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -240,7 +240,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="userAddresses">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-ipaddresses">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -271,7 +271,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tab-pane" id="userLogins">
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-tautullilogins">
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
@@ -369,23 +369,246 @@ DOCUMENTATION :: END
|
||||
<script src="${http_root}js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
|
||||
% if data:
|
||||
<script>
|
||||
% if str(data['user_id']).isdigit():
|
||||
var user_id = ${data['user_id']};
|
||||
% else:
|
||||
var user_id = null;
|
||||
% endif
|
||||
|
||||
var username = '${data['username'].replace("'", "\\'")}';
|
||||
</script>
|
||||
<script src="${http_root}js/moment-with-locale.js"></script>
|
||||
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/user_ips.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/sync_table.js${cache_param}"></script>
|
||||
<script src="${http_root}js/tables/login_logs.js${cache_param}"></script>
|
||||
<script>
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
$('a[href="#tabs-profile"]').on('shown.bs.tab', function() {
|
||||
var media_type = null;
|
||||
loadHistoryTable(media_type);
|
||||
});
|
||||
|
||||
function loadHistoryTable(media_type) {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
user_id: user_id,
|
||||
media_type: media_type
|
||||
};
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
|
||||
history_table.column(2).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table-UID-${data["user_id"]}', history_table);
|
||||
|
||||
$('#media_type-selection').on('change', function () {
|
||||
$('#media_type-selection > label').removeClass('active');
|
||||
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
|
||||
$(selected_filter).closest('label').addClass('active');
|
||||
media_type = $(selected_filter).val();
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
|
||||
var media_type = null;
|
||||
loadHistoryTable(media_type);
|
||||
});
|
||||
|
||||
$('a[href="#tabs-synceditems"]').on('shown.bs.tab', function() {
|
||||
// Build user sync table
|
||||
sync_table_options.ajax = {
|
||||
url: 'get_sync',
|
||||
data: function(d) {
|
||||
d.user_id = user_id;
|
||||
}
|
||||
}
|
||||
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
|
||||
sync_table.column(1).visible(false);
|
||||
|
||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
||||
|
||||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
||||
});
|
||||
|
||||
$('a[href="#tabs-ipaddresses"]').on('shown.bs.tab', function() {
|
||||
// Build user IP table
|
||||
user_ip_table_options.ajax = {
|
||||
url: 'get_user_ips',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
user_id: user_id
|
||||
};
|
||||
}
|
||||
}
|
||||
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
|
||||
|
||||
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
|
||||
});
|
||||
|
||||
$('a[href="#tabs-tautullilogins"]').on('shown.bs.tab', function() {
|
||||
// Build user login table
|
||||
login_log_table_options.ajax = {
|
||||
url: 'get_user_logins',
|
||||
data: function(d) {
|
||||
d.user_id = user_id;
|
||||
}
|
||||
}
|
||||
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
|
||||
login_log_table.columns([1, 2]).visible(false);
|
||||
|
||||
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis_login.button() ).appendTo('#button-bar-login');
|
||||
|
||||
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
||||
});
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
$("#edit-user-tooltip").tooltip();
|
||||
|
||||
// Load edit user modal
|
||||
$("#toggle-edit-user-modal").click(function() {
|
||||
$("#edit-user-tooltip").tooltip('hide');
|
||||
$.ajax({
|
||||
url: 'edit_user_dialog',
|
||||
data: { user_id: user_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#edit-user-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
% endif
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
user_id: user_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#user-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
highlightWatchedScrollerButton();
|
||||
});
|
||||
|
||||
var leftTotal = 0;
|
||||
$(".paginate").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#user-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
% if str(data['user_id']).isdigit():
|
||||
var user_id = ${data['user_id']};
|
||||
% else:
|
||||
var user_id = null;
|
||||
% endif
|
||||
// Javascript to enable link to tab
|
||||
var hash = document.location.hash;
|
||||
var prefix = "tab_";
|
||||
if (hash) {
|
||||
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
|
||||
}
|
||||
|
||||
var username = '${data['username'].replace("'", "\\'")}';
|
||||
|
||||
$("#edit-user-tooltip").tooltip();
|
||||
// Change hash for page-reload
|
||||
$('.user-info-nav a').on('shown.bs.tab', function (e) {
|
||||
window.location.hash = e.target.hash.replace("#", "#" + prefix);
|
||||
});
|
||||
|
||||
// Populate watch time stats
|
||||
$.ajax({
|
||||
@@ -407,210 +630,6 @@ DOCUMENTATION :: END
|
||||
}
|
||||
});
|
||||
|
||||
function loadHistoryTable(media_type) {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
url: 'get_history',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
user_id: user_id,
|
||||
media_type: media_type
|
||||
};
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
|
||||
history_table.column(2).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table-UID-${data["user_id"]}', history_table);
|
||||
|
||||
$('#media_type-selection').on('change', function () {
|
||||
$('#media_type-selection > label').removeClass('active');
|
||||
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
|
||||
$(selected_filter).closest('label').addClass('active');
|
||||
media_type = $(selected_filter).val();
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
$( "#history-tab-btn" ).one( "click", function() {
|
||||
var media_type = null;
|
||||
loadHistoryTable(media_type);
|
||||
});
|
||||
|
||||
$( "#ip-tab-btn" ).one( "click", function() {
|
||||
// Build user IP table
|
||||
user_ip_table_options.ajax = {
|
||||
url: 'get_user_ips',
|
||||
type: 'post',
|
||||
data: function ( d ) {
|
||||
return {
|
||||
json_data: JSON.stringify( d ),
|
||||
user_id: user_id
|
||||
};
|
||||
}
|
||||
}
|
||||
user_ip_table = $('#user_ip_table-UID-${data["user_id"]}').DataTable(user_ip_table_options);
|
||||
|
||||
clearSearchButton('user_ip_table-UID-${data["user_id"]}', user_ip_table);
|
||||
});
|
||||
|
||||
$( "#sync-tab-btn" ).one( "click", function() {
|
||||
// Build user sync table
|
||||
sync_table_options.ajax = {
|
||||
url: 'get_sync',
|
||||
data: function(d) {
|
||||
d.user_id = user_id;
|
||||
}
|
||||
}
|
||||
sync_table = $('#sync_table-UID-${data["user_id"]}').DataTable(sync_table_options);
|
||||
sync_table.column(1).visible(false);
|
||||
|
||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
||||
|
||||
clearSearchButton('sync_table-UID-${data["user_id"]}', sync_table);
|
||||
});
|
||||
|
||||
$( "#login-tab-btn" ).one( "click", function() {
|
||||
// Build user login table
|
||||
login_log_table_options.ajax = {
|
||||
url: 'get_user_logins',
|
||||
data: function(d) {
|
||||
d.user_id = user_id;
|
||||
}
|
||||
}
|
||||
login_log_table = $('#login_log_table-UID-${data["user_id"]}').DataTable(login_log_table_options);
|
||||
login_log_table.columns([1, 2]).visible(false);
|
||||
|
||||
var colvis_login = new $.fn.dataTable.ColVis( login_log_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis_login.button() ).appendTo('#button-bar-login');
|
||||
|
||||
clearSearchButton('login_log_table-UID-${data["user_id"]}', login_log_table);
|
||||
});
|
||||
|
||||
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
|
||||
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
|
||||
});
|
||||
|
||||
% if _session['user_group'] == 'admin':
|
||||
// Load edit user modal
|
||||
$("#toggle-edit-user-modal").click(function() {
|
||||
$("#edit-user-tooltip").tooltip('hide');
|
||||
$.ajax({
|
||||
url: 'edit_user_dialog',
|
||||
data: { user_id: user_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#edit-user-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
$('#row-edit-mode').on('click', function() {
|
||||
$('#row-edit-mode-alert').fadeIn(200);
|
||||
|
||||
if ($(this).hasClass('active')) {
|
||||
if (history_to_delete.length > 0) {
|
||||
$('#deleteCount').text(history_to_delete.length);
|
||||
$('#confirm-modal-delete').modal();
|
||||
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
|
||||
history_to_delete.forEach(function(row, idx) {
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
type: 'POST',
|
||||
data: { row_id: row },
|
||||
async: true,
|
||||
success: function (data) {
|
||||
var msg = "History deleted";
|
||||
showMsg(msg, false, true, 2000);
|
||||
}
|
||||
});
|
||||
});
|
||||
history_table.draw();
|
||||
});
|
||||
}
|
||||
|
||||
$('.delete-control').each(function () {
|
||||
$(this).addClass('hidden');
|
||||
$('#row-edit-mode-alert').fadeOut(200);
|
||||
});
|
||||
|
||||
} else {
|
||||
history_to_delete = [];
|
||||
$('.delete-control').each(function() {
|
||||
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
% endif
|
||||
|
||||
$("#refresh-history-list").click(function () {
|
||||
history_table.draw();
|
||||
});
|
||||
|
||||
function recentlyWatched() {
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: {
|
||||
user_id: user_id,
|
||||
limit: 50
|
||||
},
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
highlightWatchedScrollerButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
recentlyWatched();
|
||||
|
||||
function highlightWatchedScrollerButton() {
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var numElems = scroller.find("li").length;
|
||||
scroller.width(numElems * 175);
|
||||
if (scroller.width() > $("#user-recently-watched").width()) {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
} else {
|
||||
$("#recently-watched-page-right").addClass("disabled");
|
||||
}
|
||||
}
|
||||
|
||||
$(window).resize(function() {
|
||||
highlightWatchedScrollerButton();
|
||||
});
|
||||
|
||||
var leftTotal = 0;
|
||||
$(".paginate").click(function (e) {
|
||||
e.preventDefault();
|
||||
var scroller = $("#recently-watched-row-scroller");
|
||||
var containerWidth = $("#user-recently-watched").width();
|
||||
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
|
||||
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
|
||||
|
||||
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
|
||||
scroller.animate({ left: leftTotal }, 250);
|
||||
|
||||
if (leftTotal == 0) {
|
||||
$("#recently-watched-page-left").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-left").removeClass("disabled");
|
||||
}
|
||||
|
||||
if (leftTotal == leftMax) {
|
||||
$("#recently-watched-page-right").addClass("disabled").blur();
|
||||
} else {
|
||||
$("#recently-watched-page-right").removeClass("disabled");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
|
@@ -39,6 +39,10 @@ from cherrypy import wsgiserver
|
||||
try:
|
||||
from OpenSSL import SSL
|
||||
from OpenSSL import crypto
|
||||
if hasattr(SSL, 'Connection'):
|
||||
SSLConnectionType = SSL.Connection
|
||||
else:
|
||||
SSLConnectionType = SSL.ConnectionType
|
||||
except ImportError:
|
||||
SSL = None
|
||||
|
||||
@@ -244,7 +248,7 @@ class pyOpenSSLAdapter(wsgiserver.SSLAdapter):
|
||||
return ssl_environ
|
||||
|
||||
def makefile(self, sock, mode='r', bufsize=-1):
|
||||
if SSL and isinstance(sock, SSL.ConnectionType):
|
||||
if SSL and isinstance(sock, SSLConnectionType):
|
||||
timeout = sock.gettimeout()
|
||||
f = SSL_fileobject(sock, mode, bufsize)
|
||||
f.ssl_timeout = timeout
|
||||
|
@@ -36,12 +36,14 @@ import activity_handler
|
||||
import activity_pinger
|
||||
import config
|
||||
import database
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import notification_handler
|
||||
import notifiers
|
||||
import plextv
|
||||
import pmsconnect
|
||||
import users
|
||||
import versioncheck
|
||||
import plexpy.config
|
||||
|
||||
@@ -213,16 +215,15 @@ def initialize(config_file):
|
||||
|
||||
# Get the real PMS urls for SSL and remote access
|
||||
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
|
||||
plextv.get_real_pms_url()
|
||||
pmsconnect.get_server_friendly_name()
|
||||
plextv.get_server_resources()
|
||||
|
||||
# Refresh the users list on startup
|
||||
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
|
||||
plextv.refresh_users()
|
||||
users.refresh_users()
|
||||
|
||||
# Refresh the libraries list on startup
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN and CONFIG.REFRESH_LIBRARIES_ON_STARTUP:
|
||||
pmsconnect.refresh_libraries()
|
||||
libraries.refresh_libraries()
|
||||
|
||||
# Store the original umask
|
||||
UMASK = os.umask(0)
|
||||
@@ -323,14 +324,8 @@ def initialize_scheduler():
|
||||
hours=backup_hours, minutes=0, seconds=0, args=(True, True))
|
||||
|
||||
if WS_CONNECTED and CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||
#schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
|
||||
# hours=0, minutes=0, seconds=1)
|
||||
#schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
|
||||
# hours=0, minutes=0, seconds=monitor_seconds * bool(CONFIG.NOTIFY_RECENTLY_ADDED))
|
||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
|
||||
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||
hours=12 * (not bool(CONFIG.PMS_URL_MANUAL)), minutes=0, seconds=0)
|
||||
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name',
|
||||
hours=12, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
|
||||
hours=0, minutes=0, seconds=60 * bool(CONFIG.MONITOR_REMOTE_ACCESS))
|
||||
@@ -341,9 +336,9 @@ def initialize_scheduler():
|
||||
user_hours = CONFIG.REFRESH_USERS_INTERVAL if 1 <= CONFIG.REFRESH_USERS_INTERVAL <= 24 else 12
|
||||
library_hours = CONFIG.REFRESH_LIBRARIES_INTERVAL if 1 <= CONFIG.REFRESH_LIBRARIES_INTERVAL <= 24 else 12
|
||||
|
||||
schedule_job(plextv.refresh_users, 'Refresh users list',
|
||||
schedule_job(users.refresh_users, 'Refresh users list',
|
||||
hours=user_hours, minutes=0, seconds=0)
|
||||
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
|
||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||
hours=library_hours, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.check_server_response, 'Check server response',
|
||||
@@ -351,9 +346,7 @@ def initialize_scheduler():
|
||||
|
||||
else:
|
||||
# Cancel all jobs
|
||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name',
|
||||
schedule_job(plextv.get_server_resources, 'Refresh Plex server URLs',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(activity_pinger.check_server_access, 'Check for Plex remote access',
|
||||
@@ -361,9 +354,9 @@ def initialize_scheduler():
|
||||
schedule_job(activity_pinger.check_server_updates, 'Check for Plex updates',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(plextv.refresh_users, 'Refresh users list',
|
||||
schedule_job(users.refresh_users, 'Refresh users list',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
|
||||
schedule_job(libraries.refresh_libraries, 'Refresh libraries list',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
# Schedule job to reconnect websocket
|
||||
|
@@ -278,7 +278,6 @@ def check_server_response():
|
||||
|
||||
|
||||
def check_server_access():
|
||||
|
||||
with monitor_lock:
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
server_response = pms_connect.get_server_response()
|
||||
@@ -287,7 +286,7 @@ def check_server_access():
|
||||
|
||||
# Check for remote access
|
||||
if server_response:
|
||||
|
||||
|
||||
mapping_state = server_response['mapping_state']
|
||||
mapping_error = server_response['mapping_error']
|
||||
|
||||
|
@@ -360,9 +360,9 @@ class ActivityProcessor(object):
|
||||
'subtitles': session['subtitles'],
|
||||
'synced_version': session['synced_version'],
|
||||
'synced_version_profile': session['synced_version_profile'],
|
||||
'synced_version_title': session['synced_version_title'],
|
||||
'optimized_version': session['optimized_version'],
|
||||
'optimized_version_profile': session['optimized_version_profile']
|
||||
'optimized_version_profile': session['optimized_version_profile'],
|
||||
'optimized_version_title': session['optimized_version_title']
|
||||
}
|
||||
|
||||
# logger.debug(u"Tautulli ActivityProcessor :: Writing session_history_media_info transaction...")
|
||||
|
@@ -32,10 +32,10 @@ import xmltodict
|
||||
import plexpy
|
||||
import config
|
||||
import database
|
||||
import libraries
|
||||
import logger
|
||||
import mobile_app
|
||||
import plextv
|
||||
import pmsconnect
|
||||
import users
|
||||
|
||||
|
||||
class API2:
|
||||
@@ -345,14 +345,14 @@ class API2:
|
||||
|
||||
def refresh_libraries_list(self, **kwargs):
|
||||
""" Refresh the Tautulli libraries list."""
|
||||
data = pmsconnect.refresh_libraries()
|
||||
data = libraries.refresh_libraries()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
|
||||
return data
|
||||
|
||||
def refresh_users_list(self, **kwargs):
|
||||
""" Refresh the Tautulli users list."""
|
||||
data = plextv.refresh_users()
|
||||
data = users.refresh_users()
|
||||
self._api_result_type = 'success' if data else 'error'
|
||||
|
||||
return data
|
||||
|
@@ -140,10 +140,10 @@ SCHEDULER_LIST = ['Check GitHub for updates',
|
||||
'Check for recently added items',
|
||||
'Check for Plex updates',
|
||||
'Check for Plex remote access',
|
||||
'Check server response',
|
||||
'Refresh users list',
|
||||
'Refresh libraries list',
|
||||
'Refresh Plex server URLs',
|
||||
'Refresh Plex server name',
|
||||
'Backup Tautulli database',
|
||||
'Backup Tautulli config'
|
||||
]
|
||||
|
@@ -45,6 +45,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'PLEXWATCH_DATABASE': (str, 'PlexWatch', ''),
|
||||
'PMS_IDENTIFIER': (str, 'PMS', ''),
|
||||
'PMS_IP': (str, 'PMS', '127.0.0.1'),
|
||||
'PMS_IS_CLOUD': (int, 'PMS', 0),
|
||||
'PMS_IS_REMOTE': (int, 'PMS', 0),
|
||||
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
|
||||
'PMS_LOGS_LINE_CAP': (int, 'PMS', 1000),
|
||||
|
@@ -1104,7 +1104,7 @@ class DataFactory(object):
|
||||
if str(rating_key).isdigit():
|
||||
poster_key = rating_key
|
||||
elif metadata:
|
||||
if metadata['media_type'] in ('movie', 'show', 'artist'):
|
||||
if metadata['media_type'] in ('movie', 'show', 'artist', 'collection'):
|
||||
poster_key = metadata['rating_key']
|
||||
elif metadata['media_type'] in ('season', 'album'):
|
||||
poster_key = metadata['rating_key']
|
||||
|
@@ -1,24 +1,28 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This file is part of Tautulli.
|
||||
# This file is part of PlexPy.
|
||||
#
|
||||
# Tautulli is free software: you can redistribute it and/or modify
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Tautulli is distributed in the hope that it will be useful,
|
||||
# PlexPy is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from httplib import HTTPSConnection
|
||||
from httplib import HTTPConnection
|
||||
import ssl
|
||||
from functools import partial
|
||||
from multiprocessing.dummy import Pool as ThreadPool
|
||||
from urlparse import urljoin
|
||||
|
||||
import certifi
|
||||
from requests.packages import urllib3
|
||||
from requests.packages.urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
import plexpy
|
||||
import helpers
|
||||
@@ -30,94 +34,144 @@ class HTTPHandler(object):
|
||||
Retrieve data from Plex Server
|
||||
"""
|
||||
|
||||
def __init__(self, host, port, token, ssl_verify=True):
|
||||
self.host = host
|
||||
self.port = str(port)
|
||||
def __init__(self, urls, token=None, timeout=10, ssl_verify=True):
|
||||
if isinstance(urls, basestring):
|
||||
self.urls = urls.split() or urls.split(',')
|
||||
else:
|
||||
self.urls = urls
|
||||
|
||||
self.token = token
|
||||
if self.token:
|
||||
self.headers = {'X-Plex-Token': self.token}
|
||||
else:
|
||||
self.headers = {}
|
||||
|
||||
self.timeout = timeout
|
||||
self.ssl_verify = ssl_verify
|
||||
|
||||
"""
|
||||
Handle the HTTP requests.
|
||||
self.valid_request_types = ('GET', 'POST', 'PUT', 'DELETE')
|
||||
|
||||
Output: object
|
||||
"""
|
||||
def make_request(self,
|
||||
uri=None, proto='HTTP',
|
||||
request_type='GET',
|
||||
uri=None,
|
||||
headers=None,
|
||||
request_type='GET',
|
||||
output_format='raw',
|
||||
return_type=False,
|
||||
no_token=False,
|
||||
timeout=None):
|
||||
timeout=None,
|
||||
callback=None):
|
||||
"""
|
||||
Handle the HTTP requests.
|
||||
|
||||
if timeout is None:
|
||||
timeout = plexpy.CONFIG.PMS_TIMEOUT
|
||||
Output: list
|
||||
"""
|
||||
|
||||
valid_request_types = ['GET', 'POST', 'PUT', 'DELETE']
|
||||
self.uri = uri
|
||||
self.request_type = request_type.upper()
|
||||
self.output_format = output_format.lower()
|
||||
self.return_type = return_type
|
||||
self.callback = callback
|
||||
self.timeout = timeout or self.timeout
|
||||
|
||||
if request_type.upper() not in valid_request_types:
|
||||
if self.request_type not in self.valid_request_types:
|
||||
logger.debug(u"HTTP request made but unsupported request type given.")
|
||||
return None
|
||||
|
||||
if uri:
|
||||
if proto.upper() == 'HTTPS':
|
||||
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
|
||||
context = ssl._create_unverified_context()
|
||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout, context=context)
|
||||
logger.warn(u"Tautulli HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
|
||||
else:
|
||||
handler = HTTPSConnection(host=self.host, port=self.port, timeout=timeout)
|
||||
else:
|
||||
handler = HTTPConnection(host=self.host, port=self.port, timeout=timeout)
|
||||
request_urls = [urljoin(url, self.uri) for url in self.urls]
|
||||
|
||||
if not no_token:
|
||||
if headers:
|
||||
headers.update({'X-Plex-Token': self.token})
|
||||
else:
|
||||
headers = {'X-Plex-Token': self.token}
|
||||
if no_token and headers:
|
||||
self.headers = headers
|
||||
elif headers:
|
||||
self.headers.update(headers)
|
||||
|
||||
try:
|
||||
if headers:
|
||||
handler.request(request_type, uri, headers=headers)
|
||||
else:
|
||||
handler.request(request_type, uri)
|
||||
response = handler.getresponse()
|
||||
request_status = response.status
|
||||
request_content = response.read()
|
||||
content_type = response.getheader('content-type')
|
||||
except IOError as e:
|
||||
logger.warn(u"Failed to access uri endpoint %s with error %s" % (uri, e))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (uri, e))
|
||||
return None
|
||||
except:
|
||||
logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % uri)
|
||||
return None
|
||||
responses = []
|
||||
for r in self._http_requests_pool(request_urls):
|
||||
responses.append(r)
|
||||
|
||||
if request_status in (200, 201):
|
||||
try:
|
||||
if output_format == 'dict':
|
||||
output = helpers.convert_xml_to_dict(request_content)
|
||||
elif output_format == 'json':
|
||||
output = helpers.convert_xml_to_json(request_content)
|
||||
elif output_format == 'xml':
|
||||
output = helpers.parse_xml(request_content)
|
||||
else:
|
||||
output = request_content
|
||||
return responses[0]
|
||||
|
||||
if return_type:
|
||||
return output, content_type
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(u"Failed format response from uri %s to %s error %s" % (uri, output_format, e))
|
||||
return None
|
||||
|
||||
else:
|
||||
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (uri, request_status))
|
||||
return None
|
||||
else:
|
||||
logger.debug(u"HTTP request made but no enpoint given.")
|
||||
return None
|
||||
|
||||
def _http_requests_pool(self, urls, workers=10, chunk=None):
|
||||
"""Generator function to request urls in chunks"""
|
||||
# From cpython
|
||||
if chunk is None:
|
||||
chunk, extra = divmod(len(urls), workers * 4)
|
||||
if extra:
|
||||
chunk += 1
|
||||
if len(urls) == 0:
|
||||
chunk = 0
|
||||
|
||||
if self.ssl_verify:
|
||||
session = urllib3.PoolManager(cert_reqs='CERT_REQUIRED', ca_certs=certifi.where())
|
||||
else:
|
||||
urllib3.disable_warnings(InsecureRequestWarning)
|
||||
session = urllib3.PoolManager()
|
||||
part = partial(self._http_requests_urllib3, session=session)
|
||||
|
||||
if len(urls) == 1:
|
||||
yield part(urls[0])
|
||||
else:
|
||||
pool = ThreadPool(workers)
|
||||
|
||||
try:
|
||||
for work in pool.imap_unordered(part, urls, chunk):
|
||||
yield work
|
||||
except Exception as e:
|
||||
logger.error(u"Failed to yield request: %s" % e)
|
||||
finally:
|
||||
pool.close()
|
||||
pool.join()
|
||||
|
||||
def _http_requests_urllib3(self, url, session):
|
||||
"""Request the data from the url"""
|
||||
try:
|
||||
r = session.request(self.request_type, url, headers=self.headers, timeout=self.timeout)
|
||||
except IOError as e:
|
||||
logger.warn(u"Failed to access uri endpoint %s with error %s" % (self.uri, e))
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.warn(u"Failed to access uri endpoint %s. Is your server maybe accepting SSL connections only? %s" % (self.uri, e))
|
||||
return None
|
||||
except:
|
||||
logger.warn(u"Failed to access uri endpoint %s with Uncaught exception." % self.uri)
|
||||
return None
|
||||
|
||||
response_status = r.status
|
||||
response_content = r.data
|
||||
response_headers = r.headers
|
||||
|
||||
if response_status in (200, 201):
|
||||
return self._http_format_output(response_content, response_headers)
|
||||
else:
|
||||
logger.warn(u"Failed to access uri endpoint %s. Status code %r" % (self.uri, response_status))
|
||||
return None
|
||||
|
||||
def _http_format_output(self, response_content, response_headers):
|
||||
"""Formats the request response to the desired type"""
|
||||
try:
|
||||
if self.output_format == 'text':
|
||||
output = response_content.decode('utf-8', 'ignore')
|
||||
if self.output_format == 'dict':
|
||||
output = helpers.convert_xml_to_dict(response_content.decode('utf-8', 'ignore'))
|
||||
elif self.output_format == 'json':
|
||||
output = helpers.convert_xml_to_json(response_content.decode('utf-8', 'ignore'))
|
||||
elif self.output_format == 'xml':
|
||||
output = helpers.parse_xml(response_content.decode('utf-8', 'ignore'))
|
||||
else:
|
||||
output = response_content
|
||||
|
||||
if self.callback:
|
||||
return self.callback(output)
|
||||
|
||||
if self.return_type:
|
||||
return output, response_headers['Content-Type']
|
||||
|
||||
return output
|
||||
|
||||
except Exception as e:
|
||||
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.response_type, e))
|
||||
return None
|
||||
|
@@ -27,6 +27,66 @@ import pmsconnect
|
||||
import session
|
||||
|
||||
|
||||
def refresh_libraries():
|
||||
logger.info(u"Tautulli Libraries :: Requesting libraries list refresh...")
|
||||
|
||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
if not server_id:
|
||||
logger.error(u"Tautulli Libraries :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
|
||||
return
|
||||
|
||||
library_sections = pmsconnect.PmsConnect().get_library_details()
|
||||
|
||||
if library_sections:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
library_keys = []
|
||||
new_keys = []
|
||||
|
||||
for section in library_sections:
|
||||
section_keys = {'server_id': server_id,
|
||||
'section_id': section['section_id']}
|
||||
section_values = {'server_id': server_id,
|
||||
'section_id': section['section_id'],
|
||||
'section_name': section['section_name'],
|
||||
'section_type': section['section_type'],
|
||||
'thumb': section['thumb'],
|
||||
'art': section['art'],
|
||||
'count': section['count'],
|
||||
'parent_count': section.get('parent_count', None),
|
||||
'child_count': section.get('child_count', None),
|
||||
}
|
||||
|
||||
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
|
||||
|
||||
library_keys.append(section['section_id'])
|
||||
|
||||
if result == 'insert':
|
||||
new_keys.append(section['section_id'])
|
||||
|
||||
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
||||
plexpy.CONFIG.write()
|
||||
else:
|
||||
new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
#if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1:
|
||||
# # Start library section_id update on it's own thread
|
||||
# threading.Thread(target=libraries.update_section_ids).start()
|
||||
|
||||
#if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1:
|
||||
# # Start library labels update on it's own thread
|
||||
# threading.Thread(target=libraries.update_labels).start()
|
||||
|
||||
logger.info(u"Tautulli Libraries :: Libraries list refreshed.")
|
||||
return True
|
||||
else:
|
||||
logger.warn(u"Tautulli Libraries :: Unable to refresh libraries list.")
|
||||
return False
|
||||
|
||||
|
||||
def update_section_ids():
|
||||
plexpy.CONFIG.UPDATE_SECTION_IDS = -1
|
||||
|
||||
@@ -965,7 +1025,7 @@ class Libraries(object):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
# Refresh the PMS_URL to make sure the server_id is updated
|
||||
plextv.get_real_pms_url()
|
||||
plextv.get_server_resources()
|
||||
|
||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
|
||||
|
@@ -16,6 +16,7 @@
|
||||
|
||||
import arrow
|
||||
import bleach
|
||||
from collections import Counter
|
||||
from itertools import groupby
|
||||
import json
|
||||
from operator import itemgetter
|
||||
@@ -145,10 +146,17 @@ def notify_conditions(notify_action=None, stream_data=None, timeline_data=None):
|
||||
return False
|
||||
|
||||
if notify_action == 'on_concurrent':
|
||||
ap = activity_processor.ActivityProcessor()
|
||||
user_sessions = ap.get_sessions(user_id=stream_data['user_id'],
|
||||
ip_address=plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP)
|
||||
return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_current_activity()
|
||||
|
||||
user_sessions = []
|
||||
if result:
|
||||
user_sessions = [s for s in result['sessions'] if s['user_id'] == stream_data['user_id']]
|
||||
|
||||
if plexpy.CONFIG.NOTIFY_CONCURRENT_BY_IP:
|
||||
return len(Counter(s['ip_address'] for s in user_sessions)) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
||||
else:
|
||||
return len(user_sessions) >= plexpy.CONFIG.NOTIFY_CONCURRENT_THRESHOLD
|
||||
|
||||
elif notify_action == 'on_newdevice':
|
||||
data_factory = datafactory.DataFactory()
|
||||
@@ -504,7 +512,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
|
||||
|
||||
# Build Plex URL
|
||||
metadata['plex_url'] = '{web_url}#!/server/{pms_identifier}/details?key=%2Flibrary%2Fmetadata%2F{rating_key}'.format(
|
||||
web_url=plexpy.CONFIG.PMS_WEB_URL or 'https://app.plex.tv/desktop',
|
||||
web_url=plexpy.CONFIG.PMS_WEB_URL,
|
||||
pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
rating_key=rating_key)
|
||||
|
||||
|
@@ -1963,7 +1963,7 @@ class JOIN(Notifier):
|
||||
|
||||
deviceid_key = 'deviceId%s' % ('s' if len(self.config['device_id'].split(',')) > 1 else '')
|
||||
|
||||
data = {'api_key': self.config['api_key'],
|
||||
data = {'apikey': self.config['api_key'],
|
||||
deviceid_key: self.config['device_id'],
|
||||
'text': body.encode("utf-8")}
|
||||
|
||||
@@ -2783,12 +2783,12 @@ class SCRIPTS(Notifier):
|
||||
return False
|
||||
|
||||
if error:
|
||||
err = '\n '.join([l for l in error.splitlines()])
|
||||
err = '\n '.join([helpers.sanitize(l) for l in error.splitlines()])
|
||||
logger.error(u"Tautulli Notifiers :: Script error: \n %s" % err)
|
||||
return False
|
||||
|
||||
if output:
|
||||
out = '\n '.join([l for l in output.splitlines()])
|
||||
out = '\n '.join([helpers.sanitize(l) for l in output.splitlines()])
|
||||
logger.debug(u"Tautulli Notifiers :: Script returned: \n %s" % out)
|
||||
|
||||
if not self.script_killed:
|
||||
|
@@ -23,7 +23,6 @@ import activity_processor
|
||||
import database
|
||||
import helpers
|
||||
import logger
|
||||
import plextv
|
||||
import users
|
||||
|
||||
|
||||
@@ -284,7 +283,7 @@ def import_from_plexivity(database=None, table_name=None, import_ignore_interval
|
||||
|
||||
# Get the latest friends list so we can pull user id's
|
||||
try:
|
||||
plextv.refresh_users()
|
||||
users.refresh_users()
|
||||
except:
|
||||
logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.")
|
||||
return None
|
||||
|
532
plexpy/plextv.py
@@ -18,11 +18,9 @@
|
||||
|
||||
import base64
|
||||
import json
|
||||
from xml.dom import minidom
|
||||
|
||||
import plexpy
|
||||
import common
|
||||
import database
|
||||
import helpers
|
||||
import http_handler
|
||||
import logger
|
||||
@@ -31,129 +29,99 @@ import pmsconnect
|
||||
import session
|
||||
|
||||
|
||||
def refresh_users():
|
||||
logger.info(u"Tautulli PlexTV :: Requesting users list refresh...")
|
||||
result = PlexTV().get_full_users_list()
|
||||
def get_server_resources(return_presence=False):
|
||||
if not return_presence:
|
||||
logger.info(u"Tautulli PlexTV :: Requesting resources for server...")
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
user_data = users.Users()
|
||||
server = {'pms_name': plexpy.CONFIG.PMS_NAME,
|
||||
'pms_version': plexpy.CONFIG.PMS_VERSION,
|
||||
'pms_platform': plexpy.CONFIG.PMS_PLATFORM,
|
||||
'pms_ip': plexpy.CONFIG.PMS_IP,
|
||||
'pms_port': plexpy.CONFIG.PMS_PORT,
|
||||
'pms_ssl': plexpy.CONFIG.PMS_SSL,
|
||||
'pms_is_remote': plexpy.CONFIG.PMS_IS_REMOTE,
|
||||
'pms_is_cloud': plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
'pms_url': plexpy.CONFIG.PMS_URL,
|
||||
'pms_url_manual': plexpy.CONFIG.PMS_URL_MANUAL
|
||||
}
|
||||
|
||||
if result:
|
||||
for item in result:
|
||||
|
||||
shared_libraries = ''
|
||||
user_tokens = user_data.get_tokens(user_id=item['user_id'])
|
||||
if user_tokens and user_tokens['server_token']:
|
||||
pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token'])
|
||||
library_details = pms_connect.get_server_children()
|
||||
|
||||
if library_details:
|
||||
shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list'])
|
||||
else:
|
||||
shared_libraries = ''
|
||||
|
||||
control_value_dict = {"user_id": item['user_id']}
|
||||
new_value_dict = {"username": item['username'],
|
||||
"thumb": item['thumb'],
|
||||
"email": item['email'],
|
||||
"is_home_user": item['is_home_user'],
|
||||
"is_allow_sync": item['is_allow_sync'],
|
||||
"is_restricted": item['is_restricted'],
|
||||
"shared_libraries": shared_libraries,
|
||||
"filter_all": item['filter_all'],
|
||||
"filter_movies": item['filter_movies'],
|
||||
"filter_tv": item['filter_tv'],
|
||||
"filter_music": item['filter_music'],
|
||||
"filter_photos": item['filter_photos']
|
||||
}
|
||||
|
||||
# Check if we've set a custom avatar if so don't overwrite it.
|
||||
if item['user_id']:
|
||||
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
|
||||
'FROM users WHERE user_id = ?',
|
||||
[item['user_id']])
|
||||
if avatar_urls:
|
||||
if not avatar_urls[0]['custom_avatar_url'] or \
|
||||
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
|
||||
new_value_dict['custom_avatar_url'] = item['thumb']
|
||||
else:
|
||||
new_value_dict['custom_avatar_url'] = item['thumb']
|
||||
|
||||
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||
|
||||
logger.info(u"Tautulli PlexTV :: Users list refreshed.")
|
||||
return True
|
||||
if server['pms_url_manual'] and server['pms_ssl'] or server['pms_is_cloud']:
|
||||
scheme = 'https'
|
||||
else:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to refresh users list.")
|
||||
return False
|
||||
scheme = 'http'
|
||||
|
||||
|
||||
def get_real_pms_url():
|
||||
logger.info(u"Tautulli PlexTV :: Requesting URLs for server...")
|
||||
|
||||
# Reset any current PMS_URL value
|
||||
plexpy.CONFIG.__setattr__('PMS_URL', '')
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
fallback_url = 'http://{}:{}'.format(plexpy.CONFIG.PMS_IP, plexpy.CONFIG.PMS_PORT)
|
||||
fallback_url = '{scheme}://{hostname}:{port}'.format(scheme=scheme,
|
||||
hostname=server['pms_ip'],
|
||||
port=server['pms_port'])
|
||||
|
||||
plex_tv = PlexTV()
|
||||
result = plex_tv.get_server_urls(include_https=plexpy.CONFIG.PMS_SSL)
|
||||
plexpass = plex_tv.get_plexpass_status()
|
||||
result = plex_tv.get_server_connections(pms_identifier=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
pms_ip=server['pms_ip'],
|
||||
pms_port=server['pms_port'],
|
||||
include_https=server['pms_ssl'])
|
||||
|
||||
connections = []
|
||||
if result:
|
||||
plexpy.CONFIG.__setattr__('PMS_VERSION', result['version'])
|
||||
plexpy.CONFIG.__setattr__('PMS_PLATFORM', result['platform'])
|
||||
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', plexpass)
|
||||
connections = result['connections']
|
||||
connections = result.pop('connections', [])
|
||||
server.update(result)
|
||||
presence = server.pop('pms_presence', 0)
|
||||
else:
|
||||
connections = []
|
||||
presence = 0
|
||||
|
||||
if return_presence:
|
||||
return presence
|
||||
|
||||
plexpass = plex_tv.get_plexpass_status()
|
||||
server['pms_plexpass'] = int(plexpass)
|
||||
|
||||
# Only need to retrieve PMS_URL if using SSL
|
||||
if not plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL:
|
||||
if not server['pms_url_manual'] and server['pms_ssl']:
|
||||
if connections:
|
||||
if plexpy.CONFIG.PMS_IS_REMOTE:
|
||||
if server['pms_is_remote']:
|
||||
# Get all remote connections
|
||||
conns = [c for c in connections if c['local'] == '0' and 'plex.direct' in c['uri']]
|
||||
conns = [c for c in connections if
|
||||
c['local'] == '0' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])]
|
||||
else:
|
||||
# Get all local connections
|
||||
conns = [c for c in connections if c['local'] == '1' and 'plex.direct' in c['uri']]
|
||||
conns = [c for c in connections if
|
||||
c['local'] == '1' and ('plex.direct' in c['uri'] or 'plex.service' in c['uri'])]
|
||||
|
||||
if conns:
|
||||
# Get connection with matching address, otherwise return first connection
|
||||
conn = next((c for c in conns if c['address'] == plexpy.CONFIG.PMS_IP
|
||||
and c['port'] == str(plexpy.CONFIG.PMS_PORT)), conns[0])
|
||||
plexpy.CONFIG.__setattr__('PMS_URL', conn['uri'])
|
||||
plexpy.CONFIG.write()
|
||||
conn = next((c for c in conns if c['address'] == server['pms_ip']
|
||||
and c['port'] == str(server['pms_port'])), conns[0])
|
||||
server['pms_url'] = conn['uri']
|
||||
logger.info(u"Tautulli PlexTV :: Server URL retrieved.")
|
||||
|
||||
# get_server_urls() failed or PMS_URL not found, fallback url doesn't use SSL
|
||||
if not plexpy.CONFIG.PMS_URL:
|
||||
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
|
||||
plexpy.CONFIG.write()
|
||||
if not server['pms_url']:
|
||||
server['pms_url'] = fallback_url
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to retrieve server URLs. Using user-defined value without SSL.")
|
||||
|
||||
# Not using SSL, remote has no effect
|
||||
# Not using SSL, remote has no effect
|
||||
else:
|
||||
if plexpy.CONFIG.PMS_URL_MANUAL and plexpy.CONFIG.PMS_SSL:
|
||||
fallback_url = fallback_url.replace('http://', 'https://')
|
||||
|
||||
plexpy.CONFIG.__setattr__('PMS_URL', fallback_url)
|
||||
plexpy.CONFIG.write()
|
||||
server['pms_url'] = fallback_url
|
||||
logger.info(u"Tautulli PlexTV :: Using user-defined URL.")
|
||||
|
||||
plexpy.CONFIG.process_kwargs(server)
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
|
||||
class PlexTV(object):
|
||||
"""
|
||||
Plex.tv authentication
|
||||
"""
|
||||
|
||||
def __init__(self, username='', password='', token=None):
|
||||
self.protocol = 'HTTPS'
|
||||
def __init__(self, username=None, password=None, token=None):
|
||||
self.username = username
|
||||
self.password = password
|
||||
self.token = token
|
||||
|
||||
self.urls = 'https://plex.tv'
|
||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
||||
self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT
|
||||
|
||||
if not token:
|
||||
if not self.token:
|
||||
# Check if we should use the admin token, or the guest server token
|
||||
if session.get_session_user_id():
|
||||
user_data = users.Users()
|
||||
@@ -161,12 +129,14 @@ class PlexTV(object):
|
||||
self.token = user_tokens['server_token']
|
||||
else:
|
||||
self.token = plexpy.CONFIG.PMS_TOKEN
|
||||
else:
|
||||
self.token = token
|
||||
|
||||
self.request_handler = http_handler.HTTPHandler(host='plex.tv',
|
||||
port=443,
|
||||
if not self.token:
|
||||
logger.error(u"Tautulli PlexTV :: PlexTV called, but no token provided.")
|
||||
return
|
||||
|
||||
self.request_handler = http_handler.HTTPHandler(urls=self.urls,
|
||||
token=self.token,
|
||||
timeout=self.timeout,
|
||||
ssl_verify=self.ssl_verify)
|
||||
|
||||
def get_plex_auth(self, output_format='raw'):
|
||||
@@ -183,7 +153,6 @@ class PlexTV(object):
|
||||
}
|
||||
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='POST',
|
||||
headers=headers,
|
||||
output_format=output_format,
|
||||
@@ -265,7 +234,6 @@ class PlexTV(object):
|
||||
def get_plextv_friends(self, output_format=''):
|
||||
uri = '/api/users'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -274,7 +242,6 @@ class PlexTV(object):
|
||||
def get_plextv_user_details(self, output_format=''):
|
||||
uri = '/users/account'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -283,7 +250,6 @@ class PlexTV(object):
|
||||
def get_plextv_devices_list(self, output_format=''):
|
||||
uri = '/devices.xml'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -292,7 +258,6 @@ class PlexTV(object):
|
||||
def get_plextv_server_list(self, output_format=''):
|
||||
uri = '/pms/servers.xml'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -301,7 +266,6 @@ class PlexTV(object):
|
||||
def get_plextv_sync_lists(self, machine_id='', output_format=''):
|
||||
uri = '/servers/%s/sync_lists' % machine_id
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -313,7 +277,6 @@ class PlexTV(object):
|
||||
else:
|
||||
uri = '/api/resources'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -325,7 +288,6 @@ class PlexTV(object):
|
||||
else:
|
||||
uri = '/api/downloads/1.json'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -334,7 +296,6 @@ class PlexTV(object):
|
||||
def delete_plextv_device(self, device_id='', output_format=''):
|
||||
uri = '/devices/%s.xml' % device_id
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='DELETE',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -343,7 +304,6 @@ class PlexTV(object):
|
||||
def delete_plextv_device_sync_lists(self, client_id='', output_format=''):
|
||||
uri = '/devices/%s/sync_items' % client_id
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -352,197 +312,184 @@ class PlexTV(object):
|
||||
def delete_plextv_sync(self, client_id='', sync_id='', output_format=''):
|
||||
uri = '/devices/%s/sync_items/%s' % (client_id, sync_id)
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='DELETE',
|
||||
output_format=output_format)
|
||||
|
||||
return request
|
||||
|
||||
def get_full_users_list(self):
|
||||
friends_list = self.get_plextv_friends()
|
||||
own_account = self.get_plextv_user_details()
|
||||
friends_list = self.get_plextv_friends(output_format='xml')
|
||||
own_account = self.get_plextv_user_details(output_format='xml')
|
||||
users_list = []
|
||||
|
||||
try:
|
||||
xml_parse = minidom.parseString(own_account)
|
||||
xml_head = own_account.getElementsByTagName('user')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account: %s" % e)
|
||||
return []
|
||||
except:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list own account.")
|
||||
return []
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse own account XML for get_full_users_list: %s." % e)
|
||||
return {}
|
||||
|
||||
xml_head = xml_parse.getElementsByTagName('user')
|
||||
if not xml_head:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list.")
|
||||
else:
|
||||
for a in xml_head:
|
||||
own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
|
||||
"username": helpers.get_xml_attr(a, 'username'),
|
||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||
"email": helpers.get_xml_attr(a, 'email'),
|
||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||
"is_allow_sync": None,
|
||||
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
||||
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
||||
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
||||
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
||||
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
||||
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
||||
}
|
||||
for a in xml_head:
|
||||
own_details = {"user_id": helpers.get_xml_attr(a, 'id'),
|
||||
"username": helpers.get_xml_attr(a, 'username'),
|
||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||
"email": helpers.get_xml_attr(a, 'email'),
|
||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||
"is_allow_sync": None,
|
||||
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
||||
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
||||
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
||||
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
||||
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
||||
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
||||
}
|
||||
|
||||
users_list.append(own_details)
|
||||
users_list.append(own_details)
|
||||
|
||||
try:
|
||||
xml_parse = minidom.parseString(friends_list)
|
||||
xml_head = friends_list.getElementsByTagName('User')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list: %s" % e)
|
||||
return []
|
||||
except:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list friends list.")
|
||||
return []
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse friends list XML for get_full_users_list: %s." % e)
|
||||
return {}
|
||||
|
||||
xml_head = xml_parse.getElementsByTagName('User')
|
||||
if not xml_head:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_full_users_list.")
|
||||
else:
|
||||
for a in xml_head:
|
||||
friend = {"user_id": helpers.get_xml_attr(a, 'id'),
|
||||
"username": helpers.get_xml_attr(a, 'title'),
|
||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||
"email": helpers.get_xml_attr(a, 'email'),
|
||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
|
||||
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
||||
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
||||
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
||||
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
||||
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
||||
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
||||
}
|
||||
for a in xml_head:
|
||||
friend = {"user_id": helpers.get_xml_attr(a, 'id'),
|
||||
"username": helpers.get_xml_attr(a, 'title'),
|
||||
"thumb": helpers.get_xml_attr(a, 'thumb'),
|
||||
"email": helpers.get_xml_attr(a, 'email'),
|
||||
"is_home_user": helpers.get_xml_attr(a, 'home'),
|
||||
"is_allow_sync": helpers.get_xml_attr(a, 'allowSync'),
|
||||
"is_restricted": helpers.get_xml_attr(a, 'restricted'),
|
||||
"filter_all": helpers.get_xml_attr(a, 'filterAll'),
|
||||
"filter_movies": helpers.get_xml_attr(a, 'filterMovies'),
|
||||
"filter_tv": helpers.get_xml_attr(a, 'filterTelevision'),
|
||||
"filter_music": helpers.get_xml_attr(a, 'filterMusic'),
|
||||
"filter_photos": helpers.get_xml_attr(a, 'filterPhotos')
|
||||
}
|
||||
|
||||
users_list.append(friend)
|
||||
users_list.append(friend)
|
||||
|
||||
return users_list
|
||||
|
||||
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None, rating_key_filter=None):
|
||||
sync_list = self.get_plextv_sync_lists(machine_id)
|
||||
def get_synced_items(self, machine_id=None, client_id_filter=None, user_id_filter=None,
|
||||
rating_key_filter=None, sync_id_filter=None):
|
||||
|
||||
if machine_id is None:
|
||||
machine_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
|
||||
sync_list = self.get_plextv_sync_lists(machine_id, output_format='xml')
|
||||
user_data = users.Users()
|
||||
|
||||
synced_items = []
|
||||
|
||||
try:
|
||||
xml_parse = minidom.parseString(sync_list)
|
||||
xml_head = sync_list.getElementsByTagName('SyncList')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s" % e)
|
||||
return []
|
||||
except:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.")
|
||||
return []
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items: %s." % e)
|
||||
return {}
|
||||
|
||||
xml_head = xml_parse.getElementsByTagName('SyncList')
|
||||
for a in xml_head:
|
||||
client_id = helpers.get_xml_attr(a, 'clientIdentifier')
|
||||
|
||||
if not xml_head:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_synced_items.")
|
||||
else:
|
||||
for a in xml_head:
|
||||
client_id = helpers.get_xml_attr(a, 'clientIdentifier')
|
||||
# Filter by client_id
|
||||
if client_id_filter and str(client_id_filter) != client_id:
|
||||
continue
|
||||
|
||||
# Filter by client_id
|
||||
if client_id_filter and client_id_filter != client_id:
|
||||
continue
|
||||
sync_list_id = helpers.get_xml_attr(a, 'id')
|
||||
sync_device = a.getElementsByTagName('Device')
|
||||
|
||||
sync_id = helpers.get_xml_attr(a, 'id')
|
||||
sync_device = a.getElementsByTagName('Device')
|
||||
for device in sync_device:
|
||||
device_user_id = helpers.get_xml_attr(device, 'userID')
|
||||
try:
|
||||
device_username = user_data.get_details(user_id=device_user_id)['username']
|
||||
device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name']
|
||||
except:
|
||||
device_username = ''
|
||||
device_friendly_name = ''
|
||||
device_name = helpers.get_xml_attr(device, 'name')
|
||||
device_product = helpers.get_xml_attr(device, 'product')
|
||||
device_product_version = helpers.get_xml_attr(device, 'productVersion')
|
||||
device_platform = helpers.get_xml_attr(device, 'platform')
|
||||
device_platform_version = helpers.get_xml_attr(device, 'platformVersion')
|
||||
device_type = helpers.get_xml_attr(device, 'device')
|
||||
device_model = helpers.get_xml_attr(device, 'model')
|
||||
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
|
||||
|
||||
for device in sync_device:
|
||||
device_user_id = helpers.get_xml_attr(device, 'userID')
|
||||
try:
|
||||
device_username = user_data.get_details(user_id=device_user_id)['username']
|
||||
device_friendly_name = user_data.get_details(user_id=device_user_id)['friendly_name']
|
||||
except:
|
||||
device_username = ''
|
||||
device_friendly_name = ''
|
||||
device_name = helpers.get_xml_attr(device, 'name')
|
||||
device_product = helpers.get_xml_attr(device, 'product')
|
||||
device_product_version = helpers.get_xml_attr(device, 'productVersion')
|
||||
device_platform = helpers.get_xml_attr(device, 'platform')
|
||||
device_platform_version = helpers.get_xml_attr(device, 'platformVersion')
|
||||
device_type = helpers.get_xml_attr(device, 'device')
|
||||
device_model = helpers.get_xml_attr(device, 'model')
|
||||
device_last_seen = helpers.get_xml_attr(device, 'lastSeenAt')
|
||||
# Filter by user_id
|
||||
if user_id_filter and str(user_id_filter) != device_user_id:
|
||||
continue
|
||||
|
||||
# Filter by user_id
|
||||
if user_id_filter and user_id_filter != device_user_id:
|
||||
continue
|
||||
for synced in a.getElementsByTagName('SyncItems'):
|
||||
sync_item = synced.getElementsByTagName('SyncItem')
|
||||
for item in sync_item:
|
||||
|
||||
for synced in a.getElementsByTagName('SyncItems'):
|
||||
sync_item = synced.getElementsByTagName('SyncItem')
|
||||
for item in sync_item:
|
||||
for location in item.getElementsByTagName('Location'):
|
||||
clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F')
|
||||
|
||||
for location in item.getElementsByTagName('Location'):
|
||||
clean_uri = helpers.get_xml_attr(location, 'uri').split('%2F')
|
||||
rating_key = next((clean_uri[(idx + 1) % len(clean_uri)]
|
||||
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
|
||||
|
||||
rating_key = next((clean_uri[(idx + 1) % len(clean_uri)]
|
||||
for idx, item in enumerate(clean_uri) if item == 'metadata'), None)
|
||||
# Filter by rating_key
|
||||
if rating_key_filter and str(rating_key_filter) != rating_key:
|
||||
continue
|
||||
|
||||
# Filter by rating_key
|
||||
if rating_key_filter and rating_key_filter != rating_key:
|
||||
continue
|
||||
sync_id = helpers.get_xml_attr(item, 'id')
|
||||
|
||||
sync_id = helpers.get_xml_attr(item, 'id')
|
||||
sync_version = helpers.get_xml_attr(item, 'version')
|
||||
sync_root_title = helpers.get_xml_attr(item, 'rootTitle')
|
||||
sync_title = helpers.get_xml_attr(item, 'title')
|
||||
sync_metadata_type = helpers.get_xml_attr(item, 'metadataType')
|
||||
sync_content_type = helpers.get_xml_attr(item, 'contentType')
|
||||
# Filter by sync_id
|
||||
if sync_id_filter and str(sync_id_filter) != sync_id:
|
||||
continue
|
||||
|
||||
for status in item.getElementsByTagName('Status'):
|
||||
status_failure_code = helpers.get_xml_attr(status, 'failureCode')
|
||||
status_failure = helpers.get_xml_attr(status, 'failure')
|
||||
status_state = helpers.get_xml_attr(status, 'state')
|
||||
status_item_count = helpers.get_xml_attr(status, 'itemsCount')
|
||||
status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount')
|
||||
status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount')
|
||||
status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount')
|
||||
status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount')
|
||||
status_total_size = helpers.get_xml_attr(status, 'totalSize')
|
||||
status_item_download_percent_complete = helpers.get_percent(
|
||||
status_item_downloaded_count, status_item_count)
|
||||
sync_version = helpers.get_xml_attr(item, 'version')
|
||||
sync_root_title = helpers.get_xml_attr(item, 'rootTitle')
|
||||
sync_title = helpers.get_xml_attr(item, 'title')
|
||||
sync_metadata_type = helpers.get_xml_attr(item, 'metadataType')
|
||||
sync_content_type = helpers.get_xml_attr(item, 'contentType')
|
||||
|
||||
for settings in item.getElementsByTagName('MediaSettings'):
|
||||
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
|
||||
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
|
||||
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
||||
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
||||
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
|
||||
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
|
||||
for status in item.getElementsByTagName('Status'):
|
||||
status_failure_code = helpers.get_xml_attr(status, 'failureCode')
|
||||
status_failure = helpers.get_xml_attr(status, 'failure')
|
||||
status_state = helpers.get_xml_attr(status, 'state')
|
||||
status_item_count = helpers.get_xml_attr(status, 'itemsCount')
|
||||
status_item_complete_count = helpers.get_xml_attr(status, 'itemsCompleteCount')
|
||||
status_item_downloaded_count = helpers.get_xml_attr(status, 'itemsDownloadedCount')
|
||||
status_item_ready_count = helpers.get_xml_attr(status, 'itemsReadyCount')
|
||||
status_item_successful_count = helpers.get_xml_attr(status, 'itemsSuccessfulCount')
|
||||
status_total_size = helpers.get_xml_attr(status, 'totalSize')
|
||||
status_item_download_percent_complete = helpers.get_percent(
|
||||
status_item_downloaded_count, status_item_count)
|
||||
|
||||
sync_details = {"device_name": helpers.sanitize(device_name),
|
||||
"platform": helpers.sanitize(device_platform),
|
||||
"username": helpers.sanitize(device_username),
|
||||
"friendly_name": helpers.sanitize(device_friendly_name),
|
||||
"user_id": device_user_id,
|
||||
"root_title": helpers.sanitize(sync_root_title),
|
||||
"title": helpers.sanitize(sync_title),
|
||||
"metadata_type": sync_metadata_type,
|
||||
"content_type": sync_content_type,
|
||||
"rating_key": rating_key,
|
||||
"state": status_state,
|
||||
"item_count": status_item_count,
|
||||
"item_complete_count": status_item_complete_count,
|
||||
"item_downloaded_count": status_item_downloaded_count,
|
||||
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
||||
"music_bitrate": settings_music_bitrate,
|
||||
"photo_quality": settings_photo_quality,
|
||||
"video_quality": settings_video_quality,
|
||||
"total_size": status_total_size,
|
||||
"failure": status_failure,
|
||||
"client_id": client_id,
|
||||
"sync_id": sync_id
|
||||
}
|
||||
for settings in item.getElementsByTagName('MediaSettings'):
|
||||
settings_audio_boost = helpers.get_xml_attr(settings, 'audioBoost')
|
||||
settings_music_bitrate = helpers.get_xml_attr(settings, 'musicBitrate')
|
||||
settings_photo_quality = helpers.get_xml_attr(settings, 'photoQuality')
|
||||
settings_photo_resolution = helpers.get_xml_attr(settings, 'photoResolution')
|
||||
settings_video_quality = helpers.get_xml_attr(settings, 'videoQuality')
|
||||
settings_video_resolution = helpers.get_xml_attr(settings, 'videoResolution')
|
||||
|
||||
synced_items.append(sync_details)
|
||||
sync_details = {"device_name": helpers.sanitize(device_name),
|
||||
"platform": helpers.sanitize(device_platform),
|
||||
"user_id": device_user_id,
|
||||
"user": helpers.sanitize(device_friendly_name),
|
||||
"username": helpers.sanitize(device_username),
|
||||
"root_title": helpers.sanitize(sync_root_title),
|
||||
"sync_title": helpers.sanitize(sync_title),
|
||||
"metadata_type": sync_metadata_type,
|
||||
"content_type": sync_content_type,
|
||||
"rating_key": rating_key,
|
||||
"state": status_state,
|
||||
"item_count": status_item_count,
|
||||
"item_complete_count": status_item_complete_count,
|
||||
"item_downloaded_count": status_item_downloaded_count,
|
||||
"item_downloaded_percent_complete": status_item_download_percent_complete,
|
||||
"music_bitrate": settings_music_bitrate,
|
||||
"photo_quality": settings_photo_quality,
|
||||
"video_quality": settings_video_quality,
|
||||
"total_size": status_total_size,
|
||||
"failure": status_failure,
|
||||
"client_id": client_id,
|
||||
"sync_id": sync_id
|
||||
}
|
||||
|
||||
synced_items.append(sync_details)
|
||||
|
||||
return session.filter_session_info(synced_items, filter_key='user_id')
|
||||
|
||||
@@ -550,27 +497,16 @@ class PlexTV(object):
|
||||
logger.info(u"Tautulli PlexTV :: Deleting sync item '%s'." % sync_id)
|
||||
self.delete_plextv_sync(client_id=client_id, sync_id=sync_id)
|
||||
|
||||
def get_server_urls(self, include_https=True):
|
||||
def get_server_connections(self, pms_identifier='', pms_ip='', pms_port=32400, include_https=True):
|
||||
|
||||
if plexpy.CONFIG.PMS_IDENTIFIER:
|
||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
else:
|
||||
logger.error(u"Tautulli PlexTV :: Unable to retrieve server identity.")
|
||||
if not pms_identifier:
|
||||
logger.error(u"Tautulli PlexTV :: Unable to retrieve server connections: no pms_identifier provided.")
|
||||
return {}
|
||||
|
||||
plextv_resources = self.get_plextv_resources(include_https=include_https)
|
||||
|
||||
plextv_resources = self.get_plextv_resources(include_https=include_https,
|
||||
output_format='xml')
|
||||
try:
|
||||
xml_parse = minidom.parseString(plextv_resources)
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s" % e)
|
||||
return {}
|
||||
except:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls.")
|
||||
return {}
|
||||
|
||||
try:
|
||||
xml_head = xml_parse.getElementsByTagName('Device')
|
||||
xml_head = plextv_resources.getElementsByTagName('Device')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli PlexTV :: Unable to parse XML for get_server_urls: %s." % e)
|
||||
return {}
|
||||
@@ -580,16 +516,20 @@ class PlexTV(object):
|
||||
conn = []
|
||||
connections = device.getElementsByTagName('Connection')
|
||||
|
||||
server = {"platform": helpers.get_xml_attr(device, 'platform'),
|
||||
"version": helpers.get_xml_attr(device, 'productVersion')
|
||||
server = {'pms_identifier': helpers.get_xml_attr(device, 'clientIdentifier'),
|
||||
'pms_name': helpers.get_xml_attr(device, 'name'),
|
||||
'pms_version': helpers.get_xml_attr(device, 'productVersion'),
|
||||
'pms_platform': helpers.get_xml_attr(device, 'platform'),
|
||||
'pms_presence': helpers.get_xml_attr(device, 'presence'),
|
||||
'pms_is_cloud': 1 if helpers.get_xml_attr(device, 'platform') == 'Cloud' else 0
|
||||
}
|
||||
|
||||
for c in connections:
|
||||
server_details = {"protocol": helpers.get_xml_attr(c, 'protocol'),
|
||||
"address": helpers.get_xml_attr(c, 'address'),
|
||||
"port": helpers.get_xml_attr(c, 'port'),
|
||||
"uri": helpers.get_xml_attr(c, 'uri'),
|
||||
"local": helpers.get_xml_attr(c, 'local')
|
||||
server_details = {'protocol': helpers.get_xml_attr(c, 'protocol'),
|
||||
'address': helpers.get_xml_attr(c, 'address'),
|
||||
'port': helpers.get_xml_attr(c, 'port'),
|
||||
'uri': helpers.get_xml_attr(c, 'uri'),
|
||||
'local': helpers.get_xml_attr(c, 'local')
|
||||
}
|
||||
conn.append(server_details)
|
||||
|
||||
@@ -600,10 +540,10 @@ class PlexTV(object):
|
||||
|
||||
# Try to match the device
|
||||
for a in xml_head:
|
||||
if helpers.get_xml_attr(a, 'clientIdentifier') == server_id:
|
||||
if helpers.get_xml_attr(a, 'clientIdentifier') == pms_identifier:
|
||||
server = get_connections(a)
|
||||
break
|
||||
|
||||
|
||||
# Else no device match found
|
||||
if not server:
|
||||
# Try to match the PMS_IP and PMS_PORT
|
||||
@@ -612,15 +552,8 @@ class PlexTV(object):
|
||||
connections = a.getElementsByTagName('Connection')
|
||||
|
||||
for connection in connections:
|
||||
if helpers.get_xml_attr(connection, 'address') == plexpy.CONFIG.PMS_IP and \
|
||||
int(helpers.get_xml_attr(connection, 'port')) == plexpy.CONFIG.PMS_PORT:
|
||||
|
||||
plexpy.CONFIG.PMS_IDENTIFIER = helpers.get_xml_attr(a, 'clientIdentifier')
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
logger.info(u"Tautulli PlexTV :: PMS identifier changed from %s to %s."
|
||||
% (server_id, plexpy.CONFIG.PMS_IDENTIFIER))
|
||||
|
||||
if helpers.get_xml_attr(connection, 'address') == pms_ip and \
|
||||
helpers.get_xml_attr(connection, 'port') == str(pms_port):
|
||||
server = get_connections(a)
|
||||
break
|
||||
|
||||
@@ -649,7 +582,7 @@ class PlexTV(object):
|
||||
|
||||
return server_times
|
||||
|
||||
def discover(self, include_cloud=True):
|
||||
def discover(self, include_cloud=True, all_servers=False):
|
||||
""" Query plex for all servers online. Returns the ones you own in a selectize format """
|
||||
servers = self.get_plextv_resources(include_https=True, output_format='xml')
|
||||
clean_servers = []
|
||||
@@ -679,15 +612,16 @@ class PlexTV(object):
|
||||
connections = d.getElementsByTagName('Connection')
|
||||
|
||||
for c in connections:
|
||||
# If this is a remote server don't show any local IPs.
|
||||
if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
|
||||
helpers.get_xml_attr(c, 'local') == '1':
|
||||
continue
|
||||
if not all_servers:
|
||||
# If this is a remote server don't show any local IPs.
|
||||
if helpers.get_xml_attr(d, 'publicAddressMatches') == '0' and \
|
||||
helpers.get_xml_attr(c, 'local') == '1':
|
||||
continue
|
||||
|
||||
# If this is a local server don't show any remote IPs.
|
||||
if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \
|
||||
helpers.get_xml_attr(c, 'local') == '0':
|
||||
continue
|
||||
# If this is a local server don't show any remote IPs.
|
||||
if helpers.get_xml_attr(d, 'publicAddressMatches') == '1' and \
|
||||
helpers.get_xml_attr(c, 'local') == '0':
|
||||
continue
|
||||
|
||||
server = {'httpsRequired': helpers.get_xml_attr(d, 'httpsRequired'),
|
||||
'clientIdentifier': helpers.get_xml_attr(d, 'clientIdentifier'),
|
||||
@@ -770,8 +704,6 @@ class PlexTV(object):
|
||||
return True
|
||||
else:
|
||||
logger.debug(u"Tautulli PlexTV :: Plex Pass subscription not found.")
|
||||
plexpy.CONFIG.__setattr__('PMS_PLEXPASS', 0)
|
||||
plexpy.CONFIG.write()
|
||||
return False
|
||||
|
||||
def get_devices_list(self):
|
||||
|
@@ -22,7 +22,6 @@ import activity_processor
|
||||
import database
|
||||
import helpers
|
||||
import logger
|
||||
import plextv
|
||||
import users
|
||||
|
||||
|
||||
@@ -275,7 +274,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
||||
|
||||
# Get the latest friends list so we can pull user id's
|
||||
try:
|
||||
plextv.refresh_users()
|
||||
users.refresh_users()
|
||||
except:
|
||||
logger.debug(u"Tautulli Importer :: Unable to refresh the users list. Aborting import.")
|
||||
return None
|
||||
|
@@ -13,16 +13,12 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import threading
|
||||
import urllib
|
||||
from urlparse import urlparse
|
||||
|
||||
import plexpy
|
||||
import common
|
||||
import database
|
||||
import helpers
|
||||
import http_handler
|
||||
import libraries
|
||||
import logger
|
||||
import plextv
|
||||
import session
|
||||
@@ -49,83 +45,23 @@ def get_server_friendly_name():
|
||||
return server_name
|
||||
|
||||
|
||||
def refresh_libraries():
|
||||
logger.info(u"Tautulli Pmsconnect :: Requesting libraries list refresh...")
|
||||
|
||||
server_id = plexpy.CONFIG.PMS_IDENTIFIER
|
||||
if not server_id:
|
||||
logger.error(u"Tautulli Pmsconnect :: No PMS identifier, cannot refresh libraries. Verify server in settings.")
|
||||
return
|
||||
|
||||
library_sections = PmsConnect().get_library_details()
|
||||
|
||||
if library_sections:
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
library_keys = []
|
||||
new_keys = []
|
||||
|
||||
for section in library_sections:
|
||||
section_keys = {'server_id': server_id,
|
||||
'section_id': section['section_id']}
|
||||
section_values = {'server_id': server_id,
|
||||
'section_id': section['section_id'],
|
||||
'section_name': section['section_name'],
|
||||
'section_type': section['section_type'],
|
||||
'thumb': section['thumb'],
|
||||
'art': section['art'],
|
||||
'count': section['count'],
|
||||
'parent_count': section.get('parent_count', None),
|
||||
'child_count': section.get('child_count', None),
|
||||
}
|
||||
|
||||
result = monitor_db.upsert('library_sections', key_dict=section_keys, value_dict=section_values)
|
||||
|
||||
library_keys.append(section['section_id'])
|
||||
|
||||
if result == 'insert':
|
||||
new_keys.append(section['section_id'])
|
||||
|
||||
if plexpy.CONFIG.HOME_LIBRARY_CARDS == ['first_run_wizard']:
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', library_keys)
|
||||
plexpy.CONFIG.write()
|
||||
else:
|
||||
new_keys = plexpy.CONFIG.HOME_LIBRARY_CARDS + new_keys
|
||||
plexpy.CONFIG.__setattr__('HOME_LIBRARY_CARDS', new_keys)
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
#if plexpy.CONFIG.UPDATE_SECTION_IDS == 1 or plexpy.CONFIG.UPDATE_SECTION_IDS == -1:
|
||||
# # Start library section_id update on it's own thread
|
||||
# threading.Thread(target=libraries.update_section_ids).start()
|
||||
|
||||
#if plexpy.CONFIG.UPDATE_LABELS == 1 or plexpy.CONFIG.UPDATE_LABELS == -1:
|
||||
# # Start library labels update on it's own thread
|
||||
# threading.Thread(target=libraries.update_labels).start()
|
||||
|
||||
logger.info(u"Tautulli Pmsconnect :: Libraries list refreshed.")
|
||||
return True
|
||||
else:
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to refresh libraries list.")
|
||||
return False
|
||||
|
||||
|
||||
class PmsConnect(object):
|
||||
"""
|
||||
Retrieve data from Plex Server
|
||||
"""
|
||||
|
||||
def __init__(self, token=None):
|
||||
if plexpy.CONFIG.PMS_URL:
|
||||
url_parsed = urlparse(plexpy.CONFIG.PMS_URL)
|
||||
hostname = url_parsed.hostname
|
||||
port = url_parsed.port
|
||||
self.protocol = url_parsed.scheme
|
||||
else:
|
||||
hostname = plexpy.CONFIG.PMS_IP
|
||||
port = plexpy.CONFIG.PMS_PORT
|
||||
self.protocol = 'http'
|
||||
def __init__(self, url=None, token=None):
|
||||
self.url = url
|
||||
self.token = token
|
||||
|
||||
if not token:
|
||||
if not self.url and plexpy.CONFIG.PMS_URL:
|
||||
self.url = plexpy.CONFIG.PMS_URL
|
||||
elif not self.url:
|
||||
self.url = 'http://{hostname}:{port}'.format(hostname=plexpy.CONFIG.PMS_IP,
|
||||
port=plexpy.CONFIG.PMS_PORT)
|
||||
self.timeout = plexpy.CONFIG.PMS_TIMEOUT
|
||||
|
||||
if not self.token:
|
||||
# Check if we should use the admin token, or the guest server token
|
||||
if session.get_session_user_id():
|
||||
user_data = users.Users()
|
||||
@@ -133,12 +69,10 @@ class PmsConnect(object):
|
||||
self.token = user_tokens['server_token']
|
||||
else:
|
||||
self.token = plexpy.CONFIG.PMS_TOKEN
|
||||
else:
|
||||
self.token = token
|
||||
|
||||
self.request_handler = http_handler.HTTPHandler(host=hostname,
|
||||
port=port,
|
||||
token=self.token)
|
||||
self.request_handler = http_handler.HTTPHandler(urls=self.url,
|
||||
token=self.token,
|
||||
timeout=self.timeout)
|
||||
|
||||
def get_sessions(self, output_format=''):
|
||||
"""
|
||||
@@ -150,7 +84,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/status/sessions'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -166,7 +99,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/status/sessions/terminate?sessionId=%s&reason=%s' % (session_id, reason)
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -183,7 +115,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/metadata/' + rating_key
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -200,7 +131,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/metadata/' + rating_key + '/children'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -217,7 +147,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (start, count)
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -234,7 +163,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/sections/%s/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s' % (section_id, start, count)
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -250,6 +178,22 @@ class PmsConnect(object):
|
||||
Output: array
|
||||
"""
|
||||
uri = '/library/metadata/' + rating_key + '/children'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
return request
|
||||
|
||||
def get_children_list_related(self, rating_key='', output_format=''):
|
||||
"""
|
||||
Return list of related children in requested collection item.
|
||||
|
||||
Parameters required: rating_key { ratingKey of parent }
|
||||
Optional parameters: output_format { dict, json }
|
||||
|
||||
Output: array
|
||||
"""
|
||||
uri = '/hubs/metadata/' + rating_key + '/related'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
@@ -268,7 +212,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/metadata/' + rating_key + '/allLeaves'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -284,7 +227,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/servers'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -300,7 +242,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/:/prefs'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -316,7 +257,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/identity'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -332,7 +272,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/sections'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -351,7 +290,6 @@ class PmsConnect(object):
|
||||
|
||||
uri = '/library/sections/' + section_id + '/' + list_type + '?X-Plex-Container-Start=0' + count + sort_type + label_key
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -367,7 +305,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/library/sections/' + section_id + '/label'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -384,7 +321,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/sync/items/' + sync_id
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -400,7 +336,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/sync/transcodeQueue'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -414,9 +349,8 @@ class PmsConnect(object):
|
||||
|
||||
Output: array
|
||||
"""
|
||||
uri = '/hubs/search?query=' + urllib.quote(query.encode('utf8')) + '&limit=' + limit
|
||||
uri = '/hubs/search?query=' + urllib.quote(query.encode('utf8')) + '&limit=' + limit + '&includeCollections=1'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -432,7 +366,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/myplex/account'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -448,7 +381,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/myplex/refreshReachability'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='PUT')
|
||||
|
||||
return request
|
||||
@@ -463,7 +395,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/updater/check?download=0'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='PUT',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -479,7 +410,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/updater/status'
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -498,7 +428,6 @@ class PmsConnect(object):
|
||||
"""
|
||||
uri = '/hubs/home/recentlyAdded?X-Plex-Container-Start=%s&X-Plex-Container-Size=%s&type=%s' % (start, count, type)
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
@@ -607,7 +536,7 @@ class PmsConnect(object):
|
||||
try:
|
||||
xml_head = metadata.getElementsByTagName('MediaContainer')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata: %s." % e)
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_metadata_details: %s." % e)
|
||||
return {}
|
||||
|
||||
metadata = {}
|
||||
@@ -1039,6 +968,50 @@ class PmsConnect(object):
|
||||
helpers.get_xml_attr(metadata_main, 'title'))
|
||||
}
|
||||
|
||||
elif metadata_type == 'collection':
|
||||
metadata = {'media_type': metadata_type,
|
||||
'sub_media_type': helpers.get_xml_attr(metadata_main, 'subtype'),
|
||||
'section_id': section_id,
|
||||
'library_name': library_name,
|
||||
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
|
||||
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
|
||||
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
|
||||
'title': helpers.get_xml_attr(metadata_main, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
|
||||
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||
'sort_title': helpers.get_xml_attr(metadata_main, 'titleSort'),
|
||||
'media_index': helpers.get_xml_attr(metadata_main, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
|
||||
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
|
||||
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
|
||||
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
|
||||
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
|
||||
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
|
||||
'audience_rating': helpers.get_xml_attr(metadata_main, 'audienceRating'),
|
||||
'user_rating': helpers.get_xml_attr(metadata_main, 'userRating'),
|
||||
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
|
||||
'year': helpers.get_xml_attr(metadata_main, 'year'),
|
||||
'min_year': helpers.get_xml_attr(metadata_main, 'minYear'),
|
||||
'max_year': helpers.get_xml_attr(metadata_main, 'maxYear'),
|
||||
'thumb': helpers.get_xml_attr(metadata_main, 'thumb').split('?')[0],
|
||||
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
|
||||
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
|
||||
'art': helpers.get_xml_attr(metadata_main, 'art'),
|
||||
'banner': helpers.get_xml_attr(metadata_main, 'banner'),
|
||||
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
|
||||
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
|
||||
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
|
||||
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
|
||||
'child_count': helpers.get_xml_attr(metadata_main, 'childCount'),
|
||||
'directors': directors,
|
||||
'writers': writers,
|
||||
'actors': actors,
|
||||
'genres': genres,
|
||||
'labels': labels,
|
||||
'full_title': helpers.get_xml_attr(metadata_main, 'title')
|
||||
}
|
||||
|
||||
elif metadata_type == 'clip':
|
||||
metadata = {'media_type': metadata_type,
|
||||
'section_id': section_id,
|
||||
@@ -1434,11 +1407,11 @@ class PmsConnect(object):
|
||||
transcode_decision = 'direct play'
|
||||
|
||||
# Determine if a synced version is being played
|
||||
sync_id = None
|
||||
if media_type not in ('photo', 'clip') and not session.getElementsByTagName('Session') \
|
||||
and helpers.get_xml_attr(session, 'ratingKey').isdigit() and transcode_decision == 'direct play':
|
||||
plex_tv = plextv.PlexTV()
|
||||
synced_items = plex_tv.get_synced_items(machine_id=plexpy.CONFIG.PMS_IDENTIFIER,
|
||||
client_id_filter=player_details['machine_id'],
|
||||
synced_items = plex_tv.get_synced_items(client_id_filter=player_details['machine_id'],
|
||||
rating_key_filter=rating_key)
|
||||
if synced_items:
|
||||
sync_id = synced_items[0]['sync_id']
|
||||
@@ -1448,8 +1421,6 @@ class PmsConnect(object):
|
||||
synced_session_data = synced_xml_head[0].getElementsByTagName('Track')[0]
|
||||
elif synced_xml_head[0].getElementsByTagName('Video'):
|
||||
synced_session_data = synced_xml_head[0].getElementsByTagName('Video')[0]
|
||||
else:
|
||||
sync_id = None
|
||||
|
||||
# Figure out which version is being played
|
||||
if sync_id:
|
||||
@@ -1796,7 +1767,6 @@ class PmsConnect(object):
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_item_children(self, rating_key=''):
|
||||
"""
|
||||
Return processed and validated children list.
|
||||
@@ -1837,8 +1807,11 @@ class PmsConnect(object):
|
||||
for result in result_data:
|
||||
children_output = {'section_id': section_id,
|
||||
'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
|
||||
'parent_rating_key': helpers.get_xml_attr(result, 'parentRatingKey'),
|
||||
'media_index': helpers.get_xml_attr(result, 'index'),
|
||||
'title': helpers.get_xml_attr(result, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(result, 'parentTitle'),
|
||||
'year': helpers.get_xml_attr(result, 'year'),
|
||||
'thumb': helpers.get_xml_attr(result, 'thumb'),
|
||||
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
|
||||
'duration': helpers.get_xml_attr(result, 'duration')
|
||||
@@ -1853,6 +1826,72 @@ class PmsConnect(object):
|
||||
|
||||
return output
|
||||
|
||||
def get_item_children_related(self, rating_key=''):
|
||||
"""
|
||||
Return processed and validated children list.
|
||||
|
||||
Output: array
|
||||
"""
|
||||
children_data = self.get_children_list_related(rating_key, output_format='xml')
|
||||
|
||||
try:
|
||||
xml_head = children_data.getElementsByTagName('MediaContainer')
|
||||
except Exception as e:
|
||||
logger.warn(u"Tautulli Pmsconnect :: Unable to parse XML for get_item_children_related: %s." % e)
|
||||
return []
|
||||
|
||||
children_results_list = {'movie': [],
|
||||
'show': [],
|
||||
'season': [],
|
||||
'episode': [],
|
||||
'artist': [],
|
||||
'album': [],
|
||||
'track': [],
|
||||
}
|
||||
|
||||
for a in xml_head:
|
||||
section_id = helpers.get_xml_attr(a, 'librarySectionID')
|
||||
hubs = a.getElementsByTagName('Hub')
|
||||
|
||||
for h in hubs:
|
||||
size = helpers.get_xml_attr(h, 'size')
|
||||
media_type = helpers.get_xml_attr(h, 'type')
|
||||
title = helpers.get_xml_attr(h, 'title')
|
||||
hub_identifier = helpers.get_xml_attr(h, 'hubIdentifier')
|
||||
|
||||
if size == '0' or not hub_identifier.startswith('collection.related') or \
|
||||
media_type not in children_results_list.keys():
|
||||
continue
|
||||
|
||||
result_data = []
|
||||
|
||||
if h.getElementsByTagName('Video'):
|
||||
result_data = h.getElementsByTagName('Video')
|
||||
if h.getElementsByTagName('Directory'):
|
||||
result_data = h.getElementsByTagName('Directory')
|
||||
if h.getElementsByTagName('Track'):
|
||||
result_data = h.getElementsByTagName('Track')
|
||||
|
||||
for result in result_data:
|
||||
children_output = {'section_id': section_id,
|
||||
'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
|
||||
'parent_rating_key': helpers.get_xml_attr(result, 'parentRatingKey'),
|
||||
'media_index': helpers.get_xml_attr(result, 'index'),
|
||||
'title': helpers.get_xml_attr(result, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(result, 'parentTitle'),
|
||||
'year': helpers.get_xml_attr(result, 'year'),
|
||||
'thumb': helpers.get_xml_attr(result, 'thumb'),
|
||||
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
|
||||
'duration': helpers.get_xml_attr(result, 'duration')
|
||||
}
|
||||
children_results_list[media_type].append(children_output)
|
||||
|
||||
output = {'results_count': sum(len(s) for s in children_results_list.items()),
|
||||
'results_list': children_results_list,
|
||||
}
|
||||
|
||||
return output
|
||||
|
||||
def get_servers_info(self):
|
||||
"""
|
||||
Return the list of local servers.
|
||||
@@ -2206,7 +2245,6 @@ class PmsConnect(object):
|
||||
|
||||
uri = '/photo/:/transcode?%s' % urllib.urlencode(params)
|
||||
result = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
return_type=True)
|
||||
|
||||
@@ -2238,7 +2276,8 @@ class PmsConnect(object):
|
||||
'episode': [],
|
||||
'artist': [],
|
||||
'album': [],
|
||||
'track': []
|
||||
'track': [],
|
||||
'collection': []
|
||||
}
|
||||
|
||||
for a in xml_head:
|
||||
|
@@ -23,9 +23,67 @@ import datatables
|
||||
import helpers
|
||||
import logger
|
||||
import plextv
|
||||
import pmsconnect
|
||||
import session
|
||||
|
||||
|
||||
def refresh_users():
|
||||
logger.info(u"Tautulli Users :: Requesting users list refresh...")
|
||||
result = plextv.PlexTV().get_full_users_list()
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
user_data = Users()
|
||||
|
||||
if result:
|
||||
for item in result:
|
||||
|
||||
shared_libraries = ''
|
||||
user_tokens = user_data.get_tokens(user_id=item['user_id'])
|
||||
if user_tokens and user_tokens['server_token']:
|
||||
pms_connect = pmsconnect.PmsConnect(token=user_tokens['server_token'])
|
||||
library_details = pms_connect.get_server_children()
|
||||
|
||||
if library_details:
|
||||
shared_libraries = ';'.join(d['section_id'] for d in library_details['libraries_list'])
|
||||
else:
|
||||
shared_libraries = ''
|
||||
|
||||
control_value_dict = {"user_id": item['user_id']}
|
||||
new_value_dict = {"username": item['username'],
|
||||
"thumb": item['thumb'],
|
||||
"email": item['email'],
|
||||
"is_home_user": item['is_home_user'],
|
||||
"is_allow_sync": item['is_allow_sync'],
|
||||
"is_restricted": item['is_restricted'],
|
||||
"shared_libraries": shared_libraries,
|
||||
"filter_all": item['filter_all'],
|
||||
"filter_movies": item['filter_movies'],
|
||||
"filter_tv": item['filter_tv'],
|
||||
"filter_music": item['filter_music'],
|
||||
"filter_photos": item['filter_photos']
|
||||
}
|
||||
|
||||
# Check if we've set a custom avatar if so don't overwrite it.
|
||||
if item['user_id']:
|
||||
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
|
||||
'FROM users WHERE user_id = ?',
|
||||
[item['user_id']])
|
||||
if avatar_urls:
|
||||
if not avatar_urls[0]['custom_avatar_url'] or \
|
||||
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
|
||||
new_value_dict['custom_avatar_url'] = item['thumb']
|
||||
else:
|
||||
new_value_dict['custom_avatar_url'] = item['thumb']
|
||||
|
||||
monitor_db.upsert('users', new_value_dict, control_value_dict)
|
||||
|
||||
logger.info(u"Tautulli Users :: Users list refreshed.")
|
||||
return True
|
||||
else:
|
||||
logger.warn(u"Tautulli Users :: Unable to refresh users list.")
|
||||
return False
|
||||
|
||||
|
||||
class Users(object):
|
||||
|
||||
def __init__(self):
|
||||
@@ -360,7 +418,7 @@ class Users(object):
|
||||
logger.warn(u"Tautulli Users :: Unable to retrieve user %s from database. Requesting user list refresh."
|
||||
% user_id if user_id else user)
|
||||
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
|
||||
plextv.refresh_users()
|
||||
refresh_users()
|
||||
|
||||
user_details = get_user_details(user_id=user_id, user=user)
|
||||
|
||||
|
@@ -1,2 +1,2 @@
|
||||
PLEXPY_BRANCH = "beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.2-beta"
|
||||
PLEXPY_RELEASE_VERSION = "v2.0.5-beta"
|
||||
|
@@ -175,10 +175,20 @@ def checkGithub(auto_update=False):
|
||||
if plexpy.COMMITS_BEHIND > 0:
|
||||
logger.info('New version is available. You are %s commits behind' % plexpy.COMMITS_BEHIND)
|
||||
|
||||
url = 'https://api.github.com/repos/%s/plexpy/releases/latest' % plexpy.CONFIG.GIT_USER
|
||||
release = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == dict)
|
||||
url = 'https://api.github.com/repos/%s/plexpy/releases' % plexpy.CONFIG.GIT_USER
|
||||
releases = request.request_json(url, timeout=20, whitelist_status_code=404, validator=lambda x: type(x) == list)
|
||||
|
||||
if plexpy.CONFIG.GIT_BRANCH == 'master':
|
||||
release = next((r for r in releases if not r['prerelease']), releases[0])
|
||||
elif plexpy.CONFIG.GIT_BRANCH == 'beta':
|
||||
release = next((r for r in releases if r['prerelease'] and '-beta' in r['tag_name']), releases[0])
|
||||
elif plexpy.CONFIG.GIT_BRANCH == 'nightly':
|
||||
release = next((r for r in releases if r['prerelease'] and '-nightly' in r['tag_name']), releases[0])
|
||||
else:
|
||||
release = releases[0]
|
||||
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_plexpyupdate', 'plexpy_download_info': release,
|
||||
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
||||
'plexpy_update_commit': plexpy.LATEST_VERSION, 'plexpy_update_behind': plexpy.COMMITS_BEHIND})
|
||||
|
||||
if auto_update:
|
||||
logger.info('Running automatic update.')
|
||||
|
@@ -42,6 +42,7 @@ def start_thread():
|
||||
|
||||
def on_disconnect():
|
||||
activity_processor.ActivityProcessor().set_temp_stopped()
|
||||
plexpy.initialize_scheduler()
|
||||
|
||||
|
||||
def reconnect():
|
||||
@@ -148,9 +149,8 @@ def run():
|
||||
logger.info(u"Tautulli WebSocket :: Unable to get an internal response from the server, Plex server is down.")
|
||||
plexpy.NOTIFY_QUEUE.put({'notify_action': 'on_intdown'})
|
||||
plexpy.PLEX_SERVER_UP = False
|
||||
on_disconnect()
|
||||
|
||||
plexpy.initialize_scheduler()
|
||||
on_disconnect()
|
||||
|
||||
logger.debug(u"Tautulli WebSocket :: Leaving thread.")
|
||||
|
||||
|
@@ -27,9 +27,8 @@ from hashing_passwords import check_hash
|
||||
|
||||
import plexpy
|
||||
import logger
|
||||
import plextv
|
||||
from plexpy.database import MonitorDatabase
|
||||
from plexpy.users import Users
|
||||
from plexpy.users import Users, refresh_users
|
||||
from plexpy.plextv import PlexTV
|
||||
|
||||
|
||||
@@ -72,7 +71,7 @@ def user_login(username=None, password=None):
|
||||
|
||||
if result:
|
||||
# Refresh the users list to make sure we have all the correct permissions.
|
||||
plextv.refresh_users()
|
||||
refresh_users()
|
||||
# Successful login
|
||||
return True
|
||||
else:
|
||||
@@ -244,7 +243,6 @@ class AuthController(object):
|
||||
|
||||
expiry = datetime.now() + (timedelta(days=30) if remember_me == '1' else timedelta(minutes=60))
|
||||
|
||||
cherrypy.session.regenerate()
|
||||
cherrypy.request.login = username
|
||||
cherrypy.session[SESSION_KEY] = {'user_id': user_id,
|
||||
'user': username,
|
||||
|
@@ -16,10 +16,8 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib.static import serve_file, serve_download
|
||||
@@ -119,7 +117,7 @@ class WebInterface(object):
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
@addtoapi("get_server_list")
|
||||
def discover(self, token=None, include_cloud=True, **kwargs):
|
||||
def discover(self, token=None, include_cloud=True, all_servers=False, **kwargs):
|
||||
""" Get all your servers that are published to Plex.tv.
|
||||
|
||||
```
|
||||
@@ -150,12 +148,14 @@ class WebInterface(object):
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
include_cloud = not (include_cloud == 'false')
|
||||
all_servers = all_servers == 'true'
|
||||
|
||||
plex_tv = plextv.PlexTV()
|
||||
servers = plex_tv.discover(include_cloud=include_cloud)
|
||||
servers_list = plex_tv.discover(include_cloud=include_cloud,
|
||||
all_servers=all_servers)
|
||||
|
||||
if servers:
|
||||
return servers
|
||||
if servers_list:
|
||||
return servers_list
|
||||
|
||||
|
||||
##### Home #####
|
||||
@@ -170,6 +170,7 @@ class WebInterface(object):
|
||||
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
|
||||
"home_stats_recently_added_count": plexpy.CONFIG.HOME_STATS_RECENTLY_ADDED_COUNT,
|
||||
"pms_name": plexpy.CONFIG.PMS_NAME,
|
||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
"update_show_changelog": plexpy.CONFIG.UPDATE_SHOW_CHANGELOG
|
||||
}
|
||||
return serve_template(templatename="index.html", title="Home", config=config)
|
||||
@@ -262,6 +263,12 @@ class WebInterface(object):
|
||||
else:
|
||||
return {'result': 'error', 'message': 'Failed to terminate session.'}
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth(member_of("admin"))
|
||||
def return_sessions_url(self, **kwargs):
|
||||
return plexpy.CONFIG.PMS_URL + '/status/sessions?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
def home_stats(self, time_range=30, stats_type=0, stats_count=10, **kwargs):
|
||||
@@ -452,7 +459,7 @@ class WebInterface(object):
|
||||
@requireAuth(member_of("admin"))
|
||||
def refresh_libraries_list(self, **kwargs):
|
||||
""" Refresh the libraries list on it's own thread. """
|
||||
threading.Thread(target=pmsconnect.refresh_libraries).start()
|
||||
threading.Thread(target=libraries.refresh_libraries).start()
|
||||
logger.info(u"Manual libraries list refresh requested.")
|
||||
return True
|
||||
|
||||
@@ -1074,7 +1081,7 @@ class WebInterface(object):
|
||||
@requireAuth(member_of("admin"))
|
||||
def refresh_users_list(self, **kwargs):
|
||||
""" Refresh the users list on it's own thread. """
|
||||
threading.Thread(target=plextv.refresh_users).start()
|
||||
threading.Thread(target=users.refresh_users).start()
|
||||
logger.info(u"Manual users list refresh requested.")
|
||||
return True
|
||||
|
||||
@@ -2559,6 +2566,8 @@ class WebInterface(object):
|
||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||
"pms_token": plexpy.CONFIG.PMS_TOKEN,
|
||||
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
|
||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
||||
"pms_is_cloud": plexpy.CONFIG.PMS_IS_CLOUD,
|
||||
"pms_url_manual": checked(plexpy.CONFIG.PMS_URL_MANUAL),
|
||||
"pms_uuid": plexpy.CONFIG.PMS_UUID,
|
||||
"pms_web_url": plexpy.CONFIG.PMS_WEB_URL,
|
||||
@@ -2576,7 +2585,6 @@ class WebInterface(object):
|
||||
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
|
||||
"refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
|
||||
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
|
||||
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
|
||||
"notify_consecutive": checked(plexpy.CONFIG.NOTIFY_CONSECUTIVE),
|
||||
"notify_upload_posters": checked(plexpy.CONFIG.NOTIFY_UPLOAD_POSTERS),
|
||||
"notify_recently_added_upgrade": checked(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_UPGRADE),
|
||||
@@ -2733,8 +2741,7 @@ class WebInterface(object):
|
||||
|
||||
# Get new server URLs for SSL communications and get new server friendly name
|
||||
if server_changed:
|
||||
plextv.get_real_pms_url()
|
||||
pmsconnect.get_server_friendly_name()
|
||||
plextv.get_server_resources()
|
||||
web_socket.reconnect()
|
||||
|
||||
# If first run, start websocket
|
||||
@@ -2751,11 +2758,11 @@ class WebInterface(object):
|
||||
|
||||
# Refresh users table if our server IP changes.
|
||||
if refresh_libraries:
|
||||
threading.Thread(target=pmsconnect.refresh_libraries).start()
|
||||
threading.Thread(target=libraries.refresh_libraries).start()
|
||||
|
||||
# Refresh users table if our server IP changes.
|
||||
if refresh_users:
|
||||
threading.Thread(target=plextv.refresh_users).start()
|
||||
threading.Thread(target=users.refresh_users).start()
|
||||
|
||||
return {'result': 'success', 'message': 'Settings saved.'}
|
||||
|
||||
@@ -3425,16 +3432,15 @@ class WebInterface(object):
|
||||
# Fallback to checking /identity endpoint is server is unpublished
|
||||
# Cannot set SSL settings on the PMS if unpublished so 'http' is okay
|
||||
if not identifier:
|
||||
request_handler = http_handler.HTTPHandler(host=hostname,
|
||||
port=port,
|
||||
token=None)
|
||||
scheme = 'https' if ssl else 'http'
|
||||
url = '{scheme}://{hostname}:{port}'.format(scheme=scheme, hostname=hostname, port=port)
|
||||
uri = '/identity'
|
||||
|
||||
request_handler = http_handler.HTTPHandler(urls=url,
|
||||
ssl_verify=False)
|
||||
request = request_handler.make_request(uri=uri,
|
||||
proto='http',
|
||||
request_type='GET',
|
||||
output_format='xml',
|
||||
no_token=True,
|
||||
timeout=10)
|
||||
output_format='xml')
|
||||
if request:
|
||||
xml_head = request.getElementsByTagName('MediaContainer')[0]
|
||||
identifier = xml_head.getAttribute('machineIdentifier')
|
||||
@@ -3588,7 +3594,7 @@ class WebInterface(object):
|
||||
def get_item_children(self, rating_key='', **kwargs):
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_item_children(rating_key)
|
||||
result = pms_connect.get_item_children(rating_key=rating_key)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="info_children_list.html", data=result, title="Children List")
|
||||
@@ -3596,6 +3602,18 @@ class WebInterface(object):
|
||||
logger.warn(u"Unable to retrieve data for get_item_children.")
|
||||
return serve_template(templatename="info_children_list.html", data=None, title="Children List")
|
||||
|
||||
@cherrypy.expose
|
||||
@requireAuth()
|
||||
def get_item_children_related(self, rating_key='', title='', **kwargs):
|
||||
|
||||
pms_connect = pmsconnect.PmsConnect()
|
||||
result = pms_connect.get_item_children_related(rating_key=rating_key)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="info_collection_list.html", data=result, title=title)
|
||||
else:
|
||||
return serve_template(templatename="info_collection_list.html", data=None, title=title)
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.tools.json_out()
|
||||
@requireAuth()
|
||||
@@ -4422,7 +4440,8 @@ class WebInterface(object):
|
||||
|
||||
counts = {'stream_count_direct_play': 0,
|
||||
'stream_count_direct_stream': 0,
|
||||
'stream_count_transcode': 0}
|
||||
'stream_count_transcode': 0,
|
||||
'total_bandwidth': 0}
|
||||
|
||||
for s in result['sessions']:
|
||||
if s['transcode_decision'] == 'transcode':
|
||||
@@ -4432,6 +4451,8 @@ class WebInterface(object):
|
||||
else:
|
||||
counts['stream_count_direct_play'] += 1
|
||||
|
||||
counts['total_bandwidth'] += helpers.cast_to_int(s['bandwidth'])
|
||||
|
||||
result.update(counts)
|
||||
|
||||
return result
|
||||
|
@@ -69,14 +69,14 @@ def initialize(options):
|
||||
if options['http_password']:
|
||||
logger.info(u"Tautulli WebStart :: Web server authentication is enabled, username is '%s'", options['http_username'])
|
||||
if options['http_basic_auth']:
|
||||
auth_enabled = session_enabled = False
|
||||
session_enabled = auth_enabled = False
|
||||
basic_auth_enabled = True
|
||||
else:
|
||||
options_dict['tools.sessions.on'] = auth_enabled = session_enabled = True
|
||||
options_dict['tools.sessions.on'] = session_enabled = auth_enabled = True
|
||||
basic_auth_enabled = False
|
||||
cherrypy.tools.auth = cherrypy.Tool('before_handler', webauth.check_auth)
|
||||
else:
|
||||
auth_enabled = session_enabled = basic_auth_enabled = False
|
||||
session_enabled = auth_enabled = basic_auth_enabled = False
|
||||
|
||||
if options['http_root'].strip('/'):
|
||||
plexpy.HTTP_ROOT = options['http_root'] = '/' + options['http_root'].strip('/') + '/'
|
||||
@@ -93,9 +93,12 @@ def initialize(options):
|
||||
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
||||
'text/javascript', 'application/json',
|
||||
'application/javascript'],
|
||||
'tools.auth.on': auth_enabled,
|
||||
'tools.sessions.on': session_enabled,
|
||||
'tools.session.name': 'tautulli_session_id-' + plexpy.CONFIG.PMS_UUID,
|
||||
'tools.sessions.storage_type': 'file',
|
||||
'tools.sessions.storage_path': plexpy.CONFIG.CACHE_DIR,
|
||||
'tools.sessions.timeout': 30 * 24 * 60, # 30 days
|
||||
'tools.auth.on': auth_enabled,
|
||||
'tools.auth_basic.on': basic_auth_enabled,
|
||||
'tools.auth_basic.realm': 'Tautulli web server',
|
||||
'tools.auth_basic.checkpassword': cherrypy.lib.auth_basic.checkpassword_dict({
|
||||
@@ -112,8 +115,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
},
|
||||
'/images': {
|
||||
'tools.staticdir.on': True,
|
||||
@@ -123,8 +126,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
},
|
||||
'/css': {
|
||||
'tools.staticdir.on': True,
|
||||
@@ -134,8 +137,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
},
|
||||
'/fonts': {
|
||||
'tools.staticdir.on': True,
|
||||
@@ -145,8 +148,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
},
|
||||
'/js': {
|
||||
'tools.staticdir.on': True,
|
||||
@@ -156,8 +159,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
},
|
||||
'/cache': {
|
||||
'tools.staticdir.on': True,
|
||||
@@ -167,8 +170,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
},
|
||||
#'/pms_image_proxy': {
|
||||
# 'tools.staticdir.on': True,
|
||||
@@ -189,8 +192,8 @@ def initialize(options):
|
||||
'tools.caching.delay': 0,
|
||||
'tools.expires.on': True,
|
||||
'tools.expires.secs': 60 * 60 * 24 * 30, # 30 days
|
||||
'tools.auth.on': False,
|
||||
'tools.sessions.on': False
|
||||
'tools.sessions.on': False,
|
||||
'tools.auth.on': False
|
||||
}
|
||||
}
|
||||
|
||||
|