mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-14 01:02:59 -07:00
Added restart and shutdown links in settings.
No longer exposing passwords in html forms. Removed some old headphones js. Minor styling adjustments. Current activity on home screen now works. Some history table fixes and additions. Info screen for video items now works.
This commit is contained in:
parent
7dbaf46408
commit
04b290173c
16 changed files with 1163 additions and 257 deletions
|
@ -5,14 +5,7 @@
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="headerIncludes()">
|
<%def name="headerIncludes()">
|
||||||
<!--
|
|
||||||
<div id="subhead_container">
|
|
||||||
<div id="subhead_menu">
|
|
||||||
<a id="menu_link_shutdown" href="shutdown"><i class="fa fa-power-off"></i> Shut Down</a>
|
|
||||||
<a id="menu_link_shutdown" href="restart"><i class="fa fa-power-off"></i> Restart</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
-->
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
|
@ -22,7 +15,15 @@
|
||||||
<div class="span12">
|
<div class="span12">
|
||||||
<div class="wellheader-bg">
|
<div class="wellheader-bg">
|
||||||
<div class="dashboard-wellheader-no-chevron">
|
<div class="dashboard-wellheader-no-chevron">
|
||||||
<h2><i class="fa fa-cog"></i> Settings</h2>
|
<div class="row-fluid">
|
||||||
|
<div class="span9"><h2><i class="fa fa-cog"></i> Settings</h2></div>
|
||||||
|
<div class="span3">
|
||||||
|
<div class="pull-right">
|
||||||
|
<h5><a id="menu_link_shutdown" href="shutdown"><i class="fa fa-power-off"></i> Shut Down</a>
|
||||||
|
 <a id="menu_link_restart" href="restart"><i class="fa fa-refresh"></i> Restart</a></h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -898,7 +899,6 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
initActions();
|
|
||||||
initConfigCheckbox("#api_enabled");
|
initConfigCheckbox("#api_enabled");
|
||||||
initConfigCheckbox("#enable_https");
|
initConfigCheckbox("#enable_https");
|
||||||
|
|
||||||
|
|
|
@ -6650,7 +6650,7 @@ button.close {
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 82%;
|
width: 80%;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
float: left;
|
float: left;
|
||||||
}
|
}
|
||||||
|
@ -6665,7 +6665,7 @@ button.close {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
width: 82%;
|
width: 80%;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
float: left;
|
float: left;
|
||||||
|
|
99
data/interfaces/default/current_activity.html
Normal file
99
data/interfaces/default/current_activity.html
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
% if activity is not None:
|
||||||
|
% if activity['stream_count'] != '0':
|
||||||
|
% for a in activity['sessions']:
|
||||||
|
<div class="instance" id="instance-${a['sessionKey']}">
|
||||||
|
<div class="poster">
|
||||||
|
% if a['type'] == 'track-to-do':
|
||||||
|
<div class="art-music-face" style="background-image:url(pms_image_proxy?img=${a['thumb']}&width=300&height=300)"></div>
|
||||||
|
% elif a['type'] == 'movie':
|
||||||
|
<div class="dashboard-activity-poster-face">
|
||||||
|
<img src="pms_image_proxy?img=${a['art']}&width=300&height=169"/> <!-- media artwork -->
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="dashboard-activity-poster-face">
|
||||||
|
<img src="pms_image_proxy?img=${a['thumb']}&width=300&height=169"/> <!-- media artwork -->
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
<div class='dashboard-activity-metadata-wrapper'>
|
||||||
|
<div class='dashboard-activity-instance-overlay'>
|
||||||
|
<div class='dashboard-activity-metadata-progress-minutes'>
|
||||||
|
<div class='progress progress-warning'>
|
||||||
|
<div class="bar" style="width: ${a['progressPercent']}%">${a['progressPercent']}%</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-activity-metadata-platform" id="platform-${a['sessionKey']}">
|
||||||
|
<!-- <img src="interfaces/default/images/platforms/roku.png"> platform image -->
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-activity-metadata-user">
|
||||||
|
${a['user']} is ${a['state']}
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-activity-metadata-title">
|
||||||
|
% if a['type'] == 'episode':
|
||||||
|
<a href="info?rating_key=${a['ratingKey']}">${a['grandparentTitle']} - ${a['title']}</a>
|
||||||
|
% elif a['type'] == 'movie':
|
||||||
|
<a href="info?rating_key=${a['ratingKey']}">${a['title']}</a>
|
||||||
|
% elif a['type'] == 'track':
|
||||||
|
${a['artist']} - ${a['track']}
|
||||||
|
% else:
|
||||||
|
${a['grandparentTitle']} - ${a['title']}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="stream-${a['sessionKey']}" class="collapse out">
|
||||||
|
<div class='dashboard-activity-info-details-overlay'>
|
||||||
|
<div class='dashboard-activity-info-details-content'>
|
||||||
|
% if a['type'] == 'track':
|
||||||
|
Artist: <strong>${a['artist']}</strong>
|
||||||
|
<br>
|
||||||
|
Album: <strong>${a['album']}</strong>
|
||||||
|
<br>
|
||||||
|
% endif
|
||||||
|
% if a['state'] == 'playing':
|
||||||
|
State: <strong>Playing</strong>
|
||||||
|
% elif a['state'] == 'paused':
|
||||||
|
State: <strong>Paused</strong>
|
||||||
|
% elif a['state'] == 'buffering':
|
||||||
|
State: <strong>Buffering</strong>
|
||||||
|
% endif
|
||||||
|
<br>
|
||||||
|
% if a['type'] == 'track':
|
||||||
|
% if a['audioDecision'] == 'direct play':
|
||||||
|
Stream: <strong>Direct Play</strong>
|
||||||
|
% else:
|
||||||
|
Stream: <strong>Transcoding</strong>
|
||||||
|
% endif
|
||||||
|
<br/>
|
||||||
|
Audio: <strong>${a['audioCodec']} (${a['audioChannels']}ch)</strong>
|
||||||
|
% elif a['type'] == 'episode' or a['type'] == 'movie':
|
||||||
|
% if a['videoDecision'] == 'direct play':
|
||||||
|
Stream: <strong>Direct Play</strong>
|
||||||
|
% else:
|
||||||
|
Stream: <strong>Transcoding</strong>
|
||||||
|
% endif
|
||||||
|
<br/>
|
||||||
|
Video: <strong>${a['videoDecision']} (${a['videoCodec']}) (${a['width']}x${a['height']})</strong>
|
||||||
|
<br/>
|
||||||
|
Audio: <strong>${a['audioDecision']} (${a['audioCodec']}) (${a['audioChannels']}ch)</strong>
|
||||||
|
% endif
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-activity-button-info">
|
||||||
|
<button type="button" class="btn btn-warning" data-toggle="collapse" data-target="#stream-${a['sessionKey']}">
|
||||||
|
<i class='icon-info-sign icon-white'></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
$("#platform-${a['sessionKey']}").html("<img src='" + getPlatformImagePath('${a['player']}') + "'>");
|
||||||
|
</script>
|
||||||
|
% endfor
|
||||||
|
% else:
|
||||||
|
<div class="muted">Nothing is currently being watched.</div><br>
|
||||||
|
% endif
|
||||||
|
% else:
|
||||||
|
<div class="muted">There was an error communicating with your Plex Server. Please check your settings.</div><br>
|
||||||
|
% endif
|
9
data/interfaces/default/current_activity_header.html
Normal file
9
data/interfaces/default/current_activity_header.html
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
% if activity != None:
|
||||||
|
% if activity == '0':
|
||||||
|
<h3>Activity</h3>
|
||||||
|
% else:
|
||||||
|
<h3>Activity <strong>${activity}</strong> stream(s)</h3>
|
||||||
|
% endif
|
||||||
|
% else:
|
||||||
|
<h3>Activity</h3>
|
||||||
|
% endif
|
|
@ -38,6 +38,7 @@
|
||||||
<th align='left' id="stopped"><i class='fa fa-sort'></i> Stopped</th>
|
<th align='left' id="stopped"><i class='fa fa-sort'></i> Stopped</th>
|
||||||
<th align='left' id="duration"><i class='fa fa-sort'></i> Duration</th>
|
<th align='left' id="duration"><i class='fa fa-sort'></i> Duration</th>
|
||||||
<th align='left' id="percent_complete"> Completed</th>
|
<th align='left' id="percent_complete"> Completed</th>
|
||||||
|
<th align='left' id="rating_key"> RatingKey</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
@ -158,11 +159,21 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [4],
|
"targets": [4],
|
||||||
"data":"ip_address"
|
"data":"ip_address",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if ((cellData == '') || (cellData == '0')) {
|
||||||
|
$(td).html('n/a');
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [5],
|
"targets": [5],
|
||||||
"data":"title"
|
"data":"title",
|
||||||
|
"createdCell": function (td, cellData, rowData, row, col) {
|
||||||
|
if (cellData !== '') {
|
||||||
|
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"targets": [6],
|
"targets": [6],
|
||||||
|
@ -203,6 +214,11 @@
|
||||||
return '<span class="badge">100%</span>';
|
return '<span class="badge">100%</span>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"targets": [11],
|
||||||
|
"data":"rating_key",
|
||||||
|
"visible": false
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"drawCallback": function (settings) {
|
"drawCallback": function (settings) {
|
||||||
|
|
|
@ -3,16 +3,61 @@
|
||||||
from plexpy import helpers
|
from plexpy import helpers
|
||||||
%>
|
%>
|
||||||
|
|
||||||
<%def name="body()">
|
|
||||||
<div class="container">
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<%def name="headIncludes()">
|
<%def name="headIncludes()">
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
|
<%def name="body()">
|
||||||
|
<div class="container-fluid">
|
||||||
|
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12">
|
||||||
|
<div class="wellbg">
|
||||||
|
<div class="wellheader">
|
||||||
|
<div class="dashboard-wellheader">
|
||||||
|
<div id="currentActivityHeader">
|
||||||
|
<h3>Activity</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="currentActivity">
|
||||||
|
<div class="muted">Checking for activity...</div><br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</%def>
|
||||||
|
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
|
<script>
|
||||||
|
function currentActivity() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_current_activity',
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#currentActivity").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
currentActivity();
|
||||||
|
setInterval(currentActivity, 15000);
|
||||||
|
|
||||||
|
function currentActivityHeader() {
|
||||||
|
$.ajax({
|
||||||
|
url: 'get_current_activity_header',
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#currentActivityHeader").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
currentActivityHeader();
|
||||||
|
setInterval(currentActivityHeader, 15000);
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
162
data/interfaces/default/info.html
Normal file
162
data/interfaces/default/info.html
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
<%inherit file="base.html"/>
|
||||||
|
<%!
|
||||||
|
from plexpy import helpers
|
||||||
|
%>
|
||||||
|
|
||||||
|
<%def name="headIncludes()">
|
||||||
|
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="body()">
|
||||||
|
% if metadata:
|
||||||
|
<div class="clear"></div>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Some span12 if parameter -->
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12">
|
||||||
|
<!-- end span12 if -->
|
||||||
|
<!-- if art -->
|
||||||
|
<div class="art-face" style="background-image:url(pms_image_proxy?img=${metadata['art']}&width=1920&height=1080)">
|
||||||
|
<!-- if not art
|
||||||
|
<div class="art-face">
|
||||||
|
-->
|
||||||
|
<div class="summary-wrapper">
|
||||||
|
<div class="summary-overlay">
|
||||||
|
<div class="row-fluid">
|
||||||
|
|
||||||
|
<div class="span9">
|
||||||
|
|
||||||
|
<div class="summary-content-poster hidden-phone hidden-tablet">
|
||||||
|
% if metadata['type'] == 'episode':
|
||||||
|
<img src="pms_image_proxy?img=${metadata['parentThumb']}&width=256&height=352">
|
||||||
|
% else:
|
||||||
|
<img src="pms_image_proxy?img=${metadata['thumb']}&width=256&height=352">
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="summary-content">
|
||||||
|
<div class="summary-content-title">
|
||||||
|
% if metadata['type'] == 'movie':
|
||||||
|
<h1>${metadata['title']} (${metadata['year']})</h1>
|
||||||
|
% elif metadata['type'] == 'season':
|
||||||
|
<h1>${metadata['parentTitle']} (${metadata['title']})</h1>
|
||||||
|
% elif metadata['type'] == 'episode':
|
||||||
|
<h1>${metadata['grandparentTitle']} (Season ${metadata['parentIndex']}, Episode ${metadata['index']}) "${metadata['title']}"</h1>
|
||||||
|
% else:
|
||||||
|
<h1>${metadata['title']}</h1>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
% if metadata['type'] == 'movie':
|
||||||
|
<div id="stars" class="rateit hidden-phone hidden-tablet" data-rateit-value="" data-rateit-ispreset="true" data-rateit-readonly="true"></div>
|
||||||
|
% endif
|
||||||
|
<div class="summary-content-details-wrapper">
|
||||||
|
<div class="summary-content-director">
|
||||||
|
% if metadata['type'] == 'episode' or metadata['type'] == 'movie':
|
||||||
|
% if metadata['directors']:
|
||||||
|
Directed by <strong> ${metadata['directors'][0]}
|
||||||
|
% else:
|
||||||
|
Directed by <strong> unknown
|
||||||
|
% endif
|
||||||
|
% elif metadata['type'] == 'show':
|
||||||
|
Studio <strong> ${metadata['studio']}
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-duration">
|
||||||
|
% if metadata['type'] == 'episode' or metadata['type'] == 'movie' or metadata['type'] == 'show':
|
||||||
|
Runtime <strong> ${metadata['duration']} mins</strong>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-content-rating">
|
||||||
|
% if metadata['type'] == 'episode' or metadata['type'] == 'movie' or metadata['type'] == 'show':
|
||||||
|
Rated <strong> ${metadata['contentRating']} </strong>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-summary">
|
||||||
|
% if metadata['type'] == 'episode' or metadata['type'] == 'movie' or metadata['type'] == 'show':
|
||||||
|
<p> ${metadata['summary']} </p>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% if metadata['type'] == 'episode':
|
||||||
|
<div class="span3">
|
||||||
|
<div class="summary-content-people-wrapper hidden-phone hidden-tablet">
|
||||||
|
<div class="summary-content-writers">
|
||||||
|
<h6><strong>Written by</strong></h6>
|
||||||
|
<ul>
|
||||||
|
% for writer in metadata['writers']:
|
||||||
|
% if loop.index < 5:
|
||||||
|
<li>
|
||||||
|
${writer}
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif metadata['type'] == 'movie' or metadata['type'] == 'show':
|
||||||
|
<div class="span3">
|
||||||
|
<div class="summary-content-people-wrapper hidden-phone hidden-tablet">
|
||||||
|
<div class="summary-content-actors">
|
||||||
|
<h6><strong>Genres</strong></h6>
|
||||||
|
<ul>
|
||||||
|
% for genre in metadata['genres']:
|
||||||
|
% if loop.index < 5:
|
||||||
|
<li>
|
||||||
|
${genre}
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="summary-content-people-wrapper hidden-phone hidden-tablet">
|
||||||
|
<div class="summary-content-actors">
|
||||||
|
<h6><strong>Starring</strong></h6>
|
||||||
|
<ul>
|
||||||
|
% for actor in metadata['actors']:
|
||||||
|
% if loop.index < 5:
|
||||||
|
<li>
|
||||||
|
${actor}
|
||||||
|
</li>
|
||||||
|
% endif
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% elif metadata['type'] == 'season':
|
||||||
|
<div class="span3"></div>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- span rule -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--}-->
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="clear"></div>
|
||||||
|
<div class="container-fluid">
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span10 offset1">
|
||||||
|
<h3>Error retrieving item metadata. This media may not be available in the Plex Media Server database anymore.</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
% endif
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="javascriptIncludes()">
|
||||||
|
<script src="interfaces/default/js/jquery.rateit.min.js"></script>
|
||||||
|
% if metadata['type'] == 'movie':
|
||||||
|
<script>
|
||||||
|
// Convert rating to 5 star rating type
|
||||||
|
var starRating = Math.round(${metadata['rating']} / 2)
|
||||||
|
$('#stars').attr('data-rateit-value', starRating)
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</%def>
|
2
data/interfaces/default/js/jquery.rateit.min.js
vendored
Normal file
2
data/interfaces/default/js/jquery.rateit.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,142 +1,3 @@
|
||||||
function getThumb(imgElem,id,type) {
|
|
||||||
|
|
||||||
if ( type == 'artist' ) {
|
|
||||||
var thumbURL = "getThumb?ArtistID=" + id;
|
|
||||||
// var imgURL = "getArtwork?ArtistID=" + id;
|
|
||||||
} else {
|
|
||||||
var thumbURL = "getThumb?AlbumID=" + id;
|
|
||||||
// var imgURL = "getArtwork?AlbumID=" + id;
|
|
||||||
}
|
|
||||||
// Get Data from the cache by Artist ID
|
|
||||||
$.ajax({
|
|
||||||
url: thumbURL,
|
|
||||||
cache: true,
|
|
||||||
success: function(data){
|
|
||||||
if ( data == "" ) {
|
|
||||||
var imageUrl = "interfaces/default/images/no-cover-artist.png";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var imageUrl = data;
|
|
||||||
}
|
|
||||||
$(imgElem).attr("src",imageUrl).hide().fadeIn();
|
|
||||||
// $(imgElem).wrap('<a href="'+ imgURL +'" rel="dialog" title="' + name + '"></a>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArtwork(imgElem,id,name,type) {
|
|
||||||
|
|
||||||
if ( type == 'artist' ) {
|
|
||||||
var artworkURL = "getArtwork?ArtistID=" + id;
|
|
||||||
} else {
|
|
||||||
var artworkURL = "getArtwork?AlbumID=" + id;
|
|
||||||
}
|
|
||||||
// Get Data from the cache by Artist ID
|
|
||||||
$.ajax({
|
|
||||||
url: artworkURL,
|
|
||||||
cache: true,
|
|
||||||
success: function(data){
|
|
||||||
if ( data == "" || data == undefined ) {
|
|
||||||
var imageUrl = "interfaces/default/images/no-cover-artist.png";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var imageUrl = data;
|
|
||||||
}
|
|
||||||
$(imgElem).attr("src",imageUrl).hide().fadeIn();
|
|
||||||
$(imgElem).wrap('<a href="'+ imageUrl +'" rel="dialog" title="' + name + '"></a>');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInfo(elem,id,type) {
|
|
||||||
|
|
||||||
if ( type == 'artist' ) {
|
|
||||||
var infoURL = "getInfo?ArtistID=" + id;
|
|
||||||
} else {
|
|
||||||
var infoURL = "getInfo?AlbumID=" + id;
|
|
||||||
}
|
|
||||||
// Get Data from the cache by ID
|
|
||||||
$.ajax({
|
|
||||||
url: infoURL,
|
|
||||||
cache: true,
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data){
|
|
||||||
var summary = data.Summary;
|
|
||||||
$(elem).append(summary);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getImageLinks(elem,id,type,unveil) {
|
|
||||||
if ( type == 'artist' ) {
|
|
||||||
var infoURL = "getImageLinks?ArtistID=" + id;
|
|
||||||
} else {
|
|
||||||
var infoURL = "getImageLinks?AlbumID=" + id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get Data from the cache by ID
|
|
||||||
$.ajax({
|
|
||||||
url: infoURL,
|
|
||||||
cache: true,
|
|
||||||
dataType: "json",
|
|
||||||
success: function(data){
|
|
||||||
if (!data) {
|
|
||||||
// Invalid response
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data.thumbnail) {
|
|
||||||
var thumbnail = "interfaces/default/images/no-cover-artist.png";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var thumbnail = data.thumbnail;
|
|
||||||
}
|
|
||||||
if (!data.artwork) {
|
|
||||||
var artwork = "interfaces/default/images/no-cover-artist.png";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var artwork = data.artwork;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (unveil) {
|
|
||||||
$(elem).attr("data-src", thumbnail);
|
|
||||||
$(elem).unveil();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$(elem).attr("src", thumbnail);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initHeader() {
|
|
||||||
//settings
|
|
||||||
var header = $("#container header");
|
|
||||||
var fadeSpeed = 100, fadeTo = 0.5, topDistance = 20;
|
|
||||||
var topbarME = function() { $(header).fadeTo(fadeSpeed,1); }, topbarML = function() { $(header).fadeTo(fadeSpeed,fadeTo); };
|
|
||||||
var inside = false;
|
|
||||||
//do
|
|
||||||
$(window).scroll(function() {
|
|
||||||
position = $(window).scrollTop();
|
|
||||||
if(position > topDistance && !inside) {
|
|
||||||
//add events
|
|
||||||
topbarML();
|
|
||||||
$(header).bind('mouseenter',topbarME);
|
|
||||||
$(header).bind('mouseleave',topbarML);
|
|
||||||
$("#toTop").fadeIn();
|
|
||||||
inside = true;
|
|
||||||
}
|
|
||||||
else if (position < topDistance){
|
|
||||||
topbarME();
|
|
||||||
$(header).unbind('mouseenter',topbarME);
|
|
||||||
$(header).unbind('mouseleave',topbarML);
|
|
||||||
$("#toTop").fadeOut();
|
|
||||||
inside = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function initConfigCheckbox(elem) {
|
function initConfigCheckbox(elem) {
|
||||||
var config = $(elem).parent().next();
|
var config = $(elem).parent().next();
|
||||||
if ( $(elem).is(":checked") ) {
|
if ( $(elem).is(":checked") ) {
|
||||||
|
@ -153,66 +14,6 @@ function initConfigCheckbox(elem) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function initActions() {
|
|
||||||
$("#subhead_menu #menu_link_refresh").button();
|
|
||||||
$("#subhead_menu #menu_link_edit").button();
|
|
||||||
$("#subhead_menu .menu_link_edit").button();
|
|
||||||
$("#subhead_menu #menu_link_delete" ).button();
|
|
||||||
$("#subhead_menu #menu_link_pauze").button();
|
|
||||||
$("#subhead_menu #menu_link_resume").button();
|
|
||||||
$("#subhead_menu #menu_link_getextra").button();
|
|
||||||
$("#subhead_menu #menu_link_removeextra").button();
|
|
||||||
$("#subhead_menu #menu_link_wanted" ).button();
|
|
||||||
$("#subhead_menu #menu_link_check").button();
|
|
||||||
$("#subhead_menu #menu_link_skipped").button();
|
|
||||||
$("#subhead_menu #menu_link_retry").button();
|
|
||||||
$("#subhead_menu #menu_link_new").button();
|
|
||||||
$("#subhead_menu #menu_link_shutdown").button();
|
|
||||||
$("#subhead_menu #menu_link_scan").button();
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshSubmenu() {
|
|
||||||
var url = $(location).attr('href');
|
|
||||||
$("#subhead_container").load(url + " #subhead_menu",function(){
|
|
||||||
initActions();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function refreshTable() {
|
|
||||||
var url = $(location).attr('href');
|
|
||||||
$("table.display").load(url + " table.display tbody, table.display thead", function() {
|
|
||||||
initThisPage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
function refreshLoadArtist() {
|
|
||||||
if ( $(".gradeL").length > 0 ) {
|
|
||||||
var url = $(location).attr('href');
|
|
||||||
var loadingRow = $("table.display tr.gradeL")
|
|
||||||
loadingRow.each(function(){
|
|
||||||
var row = $(this).index() + 1;
|
|
||||||
var rowLoad = $("table.display tbody tr:nth-child("+row+")");
|
|
||||||
$(rowLoad).load(url + " table.display tbody tr:nth-child("+ row +") td", function() {
|
|
||||||
if ( $(rowLoad).children("#status").text() == 'Active' ) {
|
|
||||||
// Active
|
|
||||||
$(rowLoad).removeClass('gradeL').addClass('gradeZ');
|
|
||||||
initThisPage();
|
|
||||||
} else {
|
|
||||||
// Still loading
|
|
||||||
setTimeout(function(){
|
|
||||||
refreshLoadArtist();
|
|
||||||
},3000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function refreshTab() {
|
|
||||||
var url = $(location).attr('href');
|
|
||||||
var tabId = $('.ui-tabs-panel:visible').attr("id");
|
|
||||||
$('.ui-tabs-panel:visible').load(url + " #"+ tabId, function() {
|
|
||||||
initThisPage();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function showMsg(msg,loader,timeout,ms) {
|
function showMsg(msg,loader,timeout,ms) {
|
||||||
var feedback = $("#ajaxMsg");
|
var feedback = $("#ajaxMsg");
|
||||||
|
@ -240,21 +41,6 @@ function showMsg(msg,loader,timeout,ms) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showArtistMsg(msg) {
|
|
||||||
var feedback = $("#ajaxMsg2");
|
|
||||||
update = $("#updatebar");
|
|
||||||
if ( update.is(":visible") ) {
|
|
||||||
var height = update.height() + 35;
|
|
||||||
feedback.css("bottom",height + "px");
|
|
||||||
} else {
|
|
||||||
feedback.removeAttr("style");
|
|
||||||
}
|
|
||||||
feedback.fadeIn();
|
|
||||||
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
|
|
||||||
feedback.css("padding","14px 10px")
|
|
||||||
$(feedback).prepend(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function doAjaxCall(url,elem,reload,form) {
|
function doAjaxCall(url,elem,reload,form) {
|
||||||
// Set Message
|
// Set Message
|
||||||
feedback = $("#ajaxMsg");
|
feedback = $("#ajaxMsg");
|
||||||
|
@ -312,6 +98,7 @@ function doAjaxCall(url,elem,reload,form) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
data: dataString,
|
data: dataString,
|
||||||
|
type: 'post',
|
||||||
beforeSend: function(jqXHR, settings) {
|
beforeSend: function(jqXHR, settings) {
|
||||||
// Start loader etc.
|
// Start loader etc.
|
||||||
feedback.prepend(loader);
|
feedback.prepend(loader);
|
||||||
|
@ -371,15 +158,53 @@ function resetFilters(text){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initFancybox() {
|
function getPlatformImagePath(platformName) {
|
||||||
if ( $("a[rel=dialog]").length > 0 ) {
|
|
||||||
$.getScript('interfaces/default/js/fancybox/jquery.fancybox-1.3.4.js', function() {
|
|
||||||
$("head").append("<link rel='stylesheet' href='interfaces/default/js/fancybox/jquery.fancybox-1.3.4.css'>");
|
|
||||||
$("a[rel=dialog]").fancybox();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$(document).ready(function(){
|
if (platformName.indexOf("Roku") > -1) {
|
||||||
initHeader();
|
return 'interfaces/default/images/platforms/roku.png';
|
||||||
});
|
} else if (platformName.indexOf("Apple TV") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/appletv.png';
|
||||||
|
} else if (platformName.indexOf("Firefox") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/firefox.png';
|
||||||
|
} else if (platformName.indexOf("Chromecast") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/chromecast.png';
|
||||||
|
} else if (platformName.indexOf("Chrome") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/chrome.png';
|
||||||
|
} else if (platformName.indexOf("Android") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/android.png';
|
||||||
|
} else if (platformName.indexOf("Nexus") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/android.png';
|
||||||
|
} else if (platformName.indexOf("iPad") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/ios.png';
|
||||||
|
} else if (platformName.indexOf("iPhone") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/ios.png';
|
||||||
|
} else if (platformName.indexOf("iOS") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/ios.png';
|
||||||
|
} else if (platformName.indexOf("Plex Home Theater") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/pht.png';
|
||||||
|
} else if (platformName.indexOf("Linux/RPi-XMBC") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/xbmc.png';
|
||||||
|
} else if (platformName.indexOf("Safari") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/safari.png';
|
||||||
|
} else if (platformName.indexOf("Internet Explorer") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/ie.png';
|
||||||
|
} else if (platformName.indexOf("Unknown Browser") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/dafault.png';
|
||||||
|
} else if (platformName.indexOf("Windows-XBMC") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/xbmc.png';
|
||||||
|
} else if (platformName.indexOf("Xbox") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/xbox.png';
|
||||||
|
} else if (platformName.indexOf("Samsung") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/samsung.png';
|
||||||
|
} else if (platformName.indexOf("Opera") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/opera.png';
|
||||||
|
} else if (platformName.indexOf("KODI") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/kodi.png';
|
||||||
|
} else if (platformName.indexOf("Mystery 3") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/playstation.png';
|
||||||
|
} else if (platformName.indexOf("Mystery 4") > -1) {
|
||||||
|
return 'interfaces/default/images/platforms/playstation.png';
|
||||||
|
} else {
|
||||||
|
return 'interfaces/default/images/platforms/default.png';
|
||||||
|
}
|
||||||
|
}
|
|
@ -71,7 +71,6 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
initActions();
|
|
||||||
|
|
||||||
$('#log_table').dataTable( {
|
$('#log_table').dataTable( {
|
||||||
"processing": false,
|
"processing": false,
|
||||||
|
|
|
@ -5,9 +5,16 @@
|
||||||
</%def>
|
</%def>
|
||||||
|
|
||||||
<%def name="body()">
|
<%def name="body()">
|
||||||
<div class="table_wrapper">
|
<div class="container-fluid">
|
||||||
<div id="shutdown">
|
<div class="row-fluid">
|
||||||
<h1><i class="fa fa-refresh fa-spin"></i> PlexPy is ${message}</h1>
|
<div class="span12">
|
||||||
|
<div class="wellbg">
|
||||||
|
<div class="wellheader">
|
||||||
|
<div class="dashboard-wellheader">
|
||||||
|
<h2><i class="fa fa-refresh fa-spin"></i> PlexPy is ${message}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</%def>
|
</%def>
|
|
@ -25,6 +25,7 @@ _CONFIG_DEFINITIONS = {
|
||||||
'PMS_IP': (str, 'PMS', '127.0.0.1'),
|
'PMS_IP': (str, 'PMS', '127.0.0.1'),
|
||||||
'PMS_PORT': (int, 'PMS', 32400),
|
'PMS_PORT': (int, 'PMS', 32400),
|
||||||
'PMS_PASSWORD': (str, 'PMS', ''),
|
'PMS_PASSWORD': (str, 'PMS', ''),
|
||||||
|
'PMS_TOKEN': (str, 'PMS', ''),
|
||||||
'PMS_USERNAME': (str, 'PMS', ''),
|
'PMS_USERNAME': (str, 'PMS', ''),
|
||||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||||
'API_ENABLED': (int, 'General', 0),
|
'API_ENABLED': (int, 'General', 0),
|
||||||
|
|
|
@ -26,6 +26,7 @@ import re
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
import xmltodict
|
import xmltodict
|
||||||
|
import math
|
||||||
|
|
||||||
|
|
||||||
def multikeysort(items, columns):
|
def multikeysort(items, columns):
|
||||||
|
@ -111,6 +112,12 @@ def convert_milliseconds(ms):
|
||||||
|
|
||||||
return minutes
|
return minutes
|
||||||
|
|
||||||
|
def convert_milliseconds_to_minutes(ms):
|
||||||
|
|
||||||
|
seconds = float(ms) / 1000
|
||||||
|
minutes = round(seconds / 60, 0)
|
||||||
|
|
||||||
|
return math.trunc(minutes)
|
||||||
|
|
||||||
def convert_seconds(s):
|
def convert_seconds(s):
|
||||||
|
|
||||||
|
@ -337,3 +344,15 @@ def convert_xml_to_json(xml):
|
||||||
def convert_xml_to_dict(xml):
|
def convert_xml_to_dict(xml):
|
||||||
o = xmltodict.parse(xml)
|
o = xmltodict.parse(xml)
|
||||||
return o
|
return o
|
||||||
|
|
||||||
|
|
||||||
|
def get_percent(value1, value2):
|
||||||
|
value1 = cast_to_float(value1)
|
||||||
|
value2 = cast_to_float(value2)
|
||||||
|
|
||||||
|
if value1 != 0 and value2 != 0:
|
||||||
|
percent = (value1 / value2) * 100
|
||||||
|
else:
|
||||||
|
percent = 0
|
||||||
|
|
||||||
|
return math.trunc(percent)
|
112
plexpy/plextv.py
Normal file
112
plexpy/plextv.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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 PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from plexpy import logger, helpers, common, request
|
||||||
|
|
||||||
|
from xml.dom import minidom
|
||||||
|
from httplib import HTTPSConnection
|
||||||
|
from urlparse import parse_qsl
|
||||||
|
from urllib import urlencode
|
||||||
|
from pynma import pynma
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import cherrypy
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import plexpy
|
||||||
|
import os.path
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class PlexTV(object):
|
||||||
|
"""
|
||||||
|
Plex.tv authentication
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, username='', password=''):
|
||||||
|
self.username = username
|
||||||
|
self.password = password
|
||||||
|
|
||||||
|
def get_plex_auth(self):
|
||||||
|
|
||||||
|
http_handler = HTTPSConnection("plex.tv")
|
||||||
|
base64string = base64.encodestring('%s:%s' % (self.username, self.password)).replace('\n', '')
|
||||||
|
|
||||||
|
http_handler.request("POST",
|
||||||
|
'/users/sign_in.xml',
|
||||||
|
headers={'Content-Type': 'application/xml; charset=utf-8',
|
||||||
|
'Content-Length': '0',
|
||||||
|
'X-Plex-Device-Name': 'PlexPy',
|
||||||
|
'X-Plex-Product': 'PlexPy',
|
||||||
|
'X-Plex-Version': 'v0.1 dev',
|
||||||
|
'X-Plex-Client-Identifier': 'f0864d3531d75b19fa9204eaea456515e2502017',
|
||||||
|
'Authorization': 'Basic %s' % base64string + ":"
|
||||||
|
})
|
||||||
|
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
request_body = response.read()
|
||||||
|
logger.debug(u"Plex.tv response status: %r" % request_status)
|
||||||
|
logger.debug(u"Plex.tv response headers: %r" % response.getheaders())
|
||||||
|
logger.debug(u"Plex.tv content type: %r" % response.getheader('content-type'))
|
||||||
|
logger.debug(u"Plex.tv response body: %r" % request_body)
|
||||||
|
|
||||||
|
if request_status == 201:
|
||||||
|
logger.info(u"Plex.tv connection successful.")
|
||||||
|
return request_body
|
||||||
|
elif request_status >= 400 and request_status < 500:
|
||||||
|
logger.info(u"Plex.tv request failed: %s" % response.reason)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
logger.info(u"Plex.tv notification failed serverside.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_token(self):
|
||||||
|
|
||||||
|
plextv_response = self.get_plex_auth()
|
||||||
|
|
||||||
|
if plextv_response:
|
||||||
|
try:
|
||||||
|
xml_parse = minidom.parseString(helpers.latinToAscii(plextv_response))
|
||||||
|
except IOError, e:
|
||||||
|
logger.warn("Error parsing XML for Plex.tv token: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
xml_head = xml_parse.getElementsByTagName('user')
|
||||||
|
if not xml_head:
|
||||||
|
logger.warn("Error parsing XML for Plex.tv token: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
auth_token = xml_head[0].getAttribute('authenticationToken')
|
||||||
|
|
||||||
|
return auth_token
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_plextv_user_data(self):
|
||||||
|
|
||||||
|
plextv_response = self.get_plex_auth()
|
||||||
|
|
||||||
|
if plextv_response:
|
||||||
|
try:
|
||||||
|
user_data = helpers.convert_xml_to_dict(plextv_response)
|
||||||
|
except IOError, e:
|
||||||
|
logger.warn("Error parsing XML for Plex.tv user data: %s" % e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return user_data
|
||||||
|
else:
|
||||||
|
return False
|
466
plexpy/pmsconnect.py
Normal file
466
plexpy/pmsconnect.py
Normal file
|
@ -0,0 +1,466 @@
|
||||||
|
# This file is part of PlexPy.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# 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 PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
from plexpy import logger, helpers, common, request
|
||||||
|
|
||||||
|
from xml.dom import minidom
|
||||||
|
from httplib import HTTPSConnection
|
||||||
|
from httplib import HTTPConnection
|
||||||
|
from urlparse import parse_qsl
|
||||||
|
from urllib import urlencode
|
||||||
|
from pynma import pynma
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import cherrypy
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
import plexpy
|
||||||
|
import os.path
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
|
class PmsConnect(object):
|
||||||
|
"""
|
||||||
|
Retrieve data from Plex Server
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.host = plexpy.CONFIG.PMS_IP
|
||||||
|
self.port = str(plexpy.CONFIG.PMS_PORT)
|
||||||
|
self.token = plexpy.CONFIG.PMS_TOKEN
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return base url of Plex Server.
|
||||||
|
|
||||||
|
Output: string
|
||||||
|
"""
|
||||||
|
def get_base_url(self):
|
||||||
|
if self.host != '' and self.port != '':
|
||||||
|
base_url = 'http://' + self.host + ':' + self.port
|
||||||
|
return base_url
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return current sessions.
|
||||||
|
|
||||||
|
Optional parameters: output_format { dict, json }
|
||||||
|
|
||||||
|
Output: array
|
||||||
|
"""
|
||||||
|
def get_sessions(self, output_format=''):
|
||||||
|
url_command = '/status/sessions'
|
||||||
|
http_handler = HTTPConnection(self.host, self.port, timeout=10)
|
||||||
|
|
||||||
|
http_handler.request("GET", url_command + '?X-Plex-Token=' + self.token)
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
request_content = response.read()
|
||||||
|
|
||||||
|
if output_format == 'dict':
|
||||||
|
output = helpers.convert_xml_to_dict(request_content)
|
||||||
|
elif output_format == 'json':
|
||||||
|
output = helpers.convert_xml_to_json(request_content)
|
||||||
|
else:
|
||||||
|
output = request_content
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return metadata for request item.
|
||||||
|
|
||||||
|
Parameters required: rating_key { Plex ratingKey }
|
||||||
|
Optional parameters: output_format { dict, json }
|
||||||
|
|
||||||
|
Output: array
|
||||||
|
"""
|
||||||
|
def get_metadata(self, rating_key='', output_format=''):
|
||||||
|
url_command = '/library/metadata/' + rating_key
|
||||||
|
http_handler = HTTPConnection(self.host, self.port, timeout=10)
|
||||||
|
|
||||||
|
http_handler.request("GET", url_command + '?X-Plex-Token=' + self.token)
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
request_content = response.read()
|
||||||
|
|
||||||
|
if output_format == 'dict':
|
||||||
|
output = helpers.convert_xml_to_dict(request_content)
|
||||||
|
elif output_format == 'json':
|
||||||
|
output = helpers.convert_xml_to_json(request_content)
|
||||||
|
else:
|
||||||
|
output = request_content
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return processed and validated metadata list for requested item.
|
||||||
|
|
||||||
|
Parameters required: rating_key { Plex ratingKey }
|
||||||
|
|
||||||
|
Output: array
|
||||||
|
"""
|
||||||
|
def get_metadata_details(self, rating_key=''):
|
||||||
|
metadata = self.get_metadata(rating_key)
|
||||||
|
metadata_list = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
xml_parse = minidom.parseString(metadata)
|
||||||
|
except Exception, e:
|
||||||
|
logger.warn("Error parsing XML for Plex metadata: %s" % e)
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
logger.warn("Error parsing XML for Plex metadata.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
xml_head = xml_parse.getElementsByTagName('MediaContainer')
|
||||||
|
if not xml_head:
|
||||||
|
logger.warn("Error parsing XML for Plex metadata.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for a in xml_head:
|
||||||
|
if a.getAttribute('size'):
|
||||||
|
if a.getAttribute('size') != '1':
|
||||||
|
metadata_list = {'metadata': None}
|
||||||
|
return metadata_list
|
||||||
|
|
||||||
|
if a.getElementsByTagName('Directory'):
|
||||||
|
metadata_main = a.getElementsByTagName('Directory')[0]
|
||||||
|
metadata_type = self.get_xml_attr(metadata_main, 'type')
|
||||||
|
logger.debug(u"Metadata type: %s" % metadata_type)
|
||||||
|
elif a.getElementsByTagName('Video'):
|
||||||
|
metadata_main = a.getElementsByTagName('Video')[0]
|
||||||
|
metadata_type = self.get_xml_attr(metadata_main, 'type')
|
||||||
|
logger.debug(u"Metadata type: %s" % metadata_type)
|
||||||
|
else:
|
||||||
|
logger.debug(u"Metadata failed")
|
||||||
|
|
||||||
|
genres = []
|
||||||
|
actors = []
|
||||||
|
writers = []
|
||||||
|
directors = []
|
||||||
|
|
||||||
|
if metadata_main.getElementsByTagName('Genre'):
|
||||||
|
for genre in metadata_main.getElementsByTagName('Genre'):
|
||||||
|
genres.append(self.get_xml_attr(genre, 'tag'))
|
||||||
|
logger.debug(u"Metadata genre: %s" % self.get_xml_attr(genre, 'tag'))
|
||||||
|
|
||||||
|
if metadata_main.getElementsByTagName('Role'):
|
||||||
|
for actor in metadata_main.getElementsByTagName('Role'):
|
||||||
|
actors.append(self.get_xml_attr(actor, 'tag'))
|
||||||
|
logger.debug(u"Metadata actor: %s" % self.get_xml_attr(actor, 'tag'))
|
||||||
|
|
||||||
|
if metadata_main.getElementsByTagName('Writer'):
|
||||||
|
for writer in metadata_main.getElementsByTagName('Writer'):
|
||||||
|
writers.append(self.get_xml_attr(writer, 'tag'))
|
||||||
|
logger.debug(u"Metadata genre: %s" % self.get_xml_attr(writer, 'tag'))
|
||||||
|
|
||||||
|
if metadata_main.getElementsByTagName('Director'):
|
||||||
|
for director in metadata_main.getElementsByTagName('Director'):
|
||||||
|
directors.append(self.get_xml_attr(director, 'tag'))
|
||||||
|
logger.debug(u"Metadata actor: %s" % self.get_xml_attr(director, 'tag'))
|
||||||
|
|
||||||
|
if metadata_type == 'show':
|
||||||
|
metadata = {'type': metadata_type,
|
||||||
|
'ratingKey': self.get_xml_attr(metadata_main, 'ratingKey'),
|
||||||
|
'studio': self.get_xml_attr(metadata_main, 'studio'),
|
||||||
|
'title': self.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'contentRating': self.get_xml_attr(metadata_main, 'contentRating'),
|
||||||
|
'summary': self.get_xml_attr(metadata_main, 'summary'),
|
||||||
|
'rating': self.get_xml_attr(metadata_main, 'rating'),
|
||||||
|
'duration': helpers.convert_milliseconds_to_minutes(self.get_xml_attr(metadata_main, 'duration')),
|
||||||
|
'year': self.get_xml_attr(metadata_main, 'year'),
|
||||||
|
'thumb': self.get_xml_attr(metadata_main, 'thumb'),
|
||||||
|
'art': self.get_xml_attr(metadata_main, 'art'),
|
||||||
|
'originallyAvailableAt': self.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||||
|
'writers': writers,
|
||||||
|
'directors': directors,
|
||||||
|
'genres': genres,
|
||||||
|
'actors': actors
|
||||||
|
}
|
||||||
|
metadata_list = {'metadata': metadata}
|
||||||
|
elif metadata_type == 'episode':
|
||||||
|
metadata = {'type': metadata_type,
|
||||||
|
'ratingKey': self.get_xml_attr(metadata_main, 'ratingKey'),
|
||||||
|
'grandparentTitle': self.get_xml_attr(metadata_main, 'grandparentTitle'),
|
||||||
|
'parentIndex': self.get_xml_attr(metadata_main, 'parentIndex'),
|
||||||
|
'index': self.get_xml_attr(metadata_main, 'index'),
|
||||||
|
'title': self.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'contentRating': self.get_xml_attr(metadata_main, 'contentRating'),
|
||||||
|
'summary': self.get_xml_attr(metadata_main, 'summary'),
|
||||||
|
'duration': helpers.convert_milliseconds_to_minutes(self.get_xml_attr(metadata_main, 'duration')),
|
||||||
|
'year': self.get_xml_attr(metadata_main, 'year'),
|
||||||
|
'thumb': self.get_xml_attr(metadata_main, 'thumb'),
|
||||||
|
'parentThumb': self.get_xml_attr(metadata_main, 'parentThumb'),
|
||||||
|
'art': self.get_xml_attr(metadata_main, 'art'),
|
||||||
|
'originallyAvailableAt': self.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||||
|
'writers': writers,
|
||||||
|
'directors': directors,
|
||||||
|
'genres': genres,
|
||||||
|
'actors': actors
|
||||||
|
}
|
||||||
|
metadata_list = {'metadata': metadata}
|
||||||
|
elif metadata_type == 'movie':
|
||||||
|
metadata = {'type': metadata_type,
|
||||||
|
'ratingKey': self.get_xml_attr(metadata_main, 'ratingKey'),
|
||||||
|
'studio': self.get_xml_attr(metadata_main, 'studio'),
|
||||||
|
'title': self.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'contentRating': self.get_xml_attr(metadata_main, 'contentRating'),
|
||||||
|
'summary': self.get_xml_attr(metadata_main, 'summary'),
|
||||||
|
'rating': self.get_xml_attr(metadata_main, 'rating'),
|
||||||
|
'duration': helpers.convert_milliseconds_to_minutes(self.get_xml_attr(metadata_main, 'duration')),
|
||||||
|
'year': self.get_xml_attr(metadata_main, 'year'),
|
||||||
|
'thumb': self.get_xml_attr(metadata_main, 'thumb'),
|
||||||
|
'art': self.get_xml_attr(metadata_main, 'art'),
|
||||||
|
'originallyAvailableAt': self.get_xml_attr(metadata_main, 'originallyAvailableAt'),
|
||||||
|
'genres': genres,
|
||||||
|
'actors': actors,
|
||||||
|
'writers': writers,
|
||||||
|
'directors': directors
|
||||||
|
}
|
||||||
|
metadata_list = {'metadata': metadata}
|
||||||
|
elif metadata_type == 'season':
|
||||||
|
metadata = {'type': metadata_type,
|
||||||
|
'ratingKey': self.get_xml_attr(metadata_main, 'ratingKey'),
|
||||||
|
'parentTitle': self.get_xml_attr(metadata_main, 'parentTitle'),
|
||||||
|
'index': self.get_xml_attr(metadata_main, 'index'),
|
||||||
|
'title': self.get_xml_attr(metadata_main, 'title'),
|
||||||
|
'thumb': self.get_xml_attr(metadata_main, 'thumb'),
|
||||||
|
'art': self.get_xml_attr(metadata_main, 'art'),
|
||||||
|
}
|
||||||
|
metadata_list = {'metadata': metadata}
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return metadata_list
|
||||||
|
|
||||||
|
"""
|
||||||
|
Validate xml keys to make sure they exist and return their attribute value, return blank value is none found
|
||||||
|
"""
|
||||||
|
def get_xml_attr(self, xml_key, attribute, return_bool=False, default_return=''):
|
||||||
|
if xml_key.getAttribute(attribute):
|
||||||
|
if return_bool:
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return xml_key.getAttribute(attribute)
|
||||||
|
else:
|
||||||
|
if return_bool:
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
return default_return
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return processed and validated session list.
|
||||||
|
|
||||||
|
Output: array
|
||||||
|
"""
|
||||||
|
def get_current_activity(self):
|
||||||
|
session_data = self.get_sessions()
|
||||||
|
session_list = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
xml_parse = minidom.parseString(session_data)
|
||||||
|
except Exception, e:
|
||||||
|
logger.warn("Error parsing XML for Plex session data: %s" % e)
|
||||||
|
return None
|
||||||
|
except:
|
||||||
|
logger.warn("Error parsing XML for Plex session data.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
xml_head = xml_parse.getElementsByTagName('MediaContainer')
|
||||||
|
if not xml_head:
|
||||||
|
logger.warn("Error parsing XML for Plex session data.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for a in xml_head:
|
||||||
|
if a.getAttribute('size'):
|
||||||
|
if a.getAttribute('size') == '0':
|
||||||
|
logger.debug(u"No active sessions.")
|
||||||
|
session_list = {'stream_count': '0',
|
||||||
|
'sessions': []
|
||||||
|
}
|
||||||
|
return session_list
|
||||||
|
|
||||||
|
if a.getElementsByTagName('Track'):
|
||||||
|
session_data = a.getElementsByTagName('Track')
|
||||||
|
session_type = 'track'
|
||||||
|
logger.debug(u"Track session active.")
|
||||||
|
for session in session_data:
|
||||||
|
session_output = self.get_session_each(session_type, session)
|
||||||
|
session_list.append(session_output)
|
||||||
|
if a.getElementsByTagName('Video'):
|
||||||
|
session_data = a.getElementsByTagName('Video')
|
||||||
|
session_type = 'video'
|
||||||
|
logger.debug(u"Video session active.")
|
||||||
|
for session in session_data:
|
||||||
|
session_output = self.get_session_each(session_type, session)
|
||||||
|
session_list.append(session_output)
|
||||||
|
|
||||||
|
output = {'stream_count': self.get_xml_attr(xml_head[0], 'size'),
|
||||||
|
'sessions': session_list
|
||||||
|
}
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return selected data from current sessions.
|
||||||
|
This function processes and validates session data
|
||||||
|
|
||||||
|
Parameters required: stream_type { track or video }
|
||||||
|
session { the session dictionary }
|
||||||
|
Output: dict
|
||||||
|
"""
|
||||||
|
def get_session_each(self, stream_type='', session=None):
|
||||||
|
session_output = None
|
||||||
|
if stream_type == 'track':
|
||||||
|
if session.getElementsByTagName('TranscodeSession'):
|
||||||
|
transcode_session = session.getElementsByTagName('TranscodeSession')[0]
|
||||||
|
audio_decision = self.get_xml_attr(transcode_session, 'audioDecision')
|
||||||
|
audio_channels = self.get_xml_attr(transcode_session, 'audioChannels')
|
||||||
|
audio_codec = self.get_xml_attr(transcode_session, 'audioCodec')
|
||||||
|
duration = self.get_xml_attr(transcode_session, 'duration')
|
||||||
|
progress = self.get_xml_attr(transcode_session, 'viewOffset')
|
||||||
|
else:
|
||||||
|
media_info = session.getElementsByTagName('Media')[0]
|
||||||
|
audio_decision = 'direct play'
|
||||||
|
audio_channels = self.get_xml_attr(media_info, 'audioChannels')
|
||||||
|
audio_codec = self.get_xml_attr(media_info, 'audioCodec')
|
||||||
|
duration = self.get_xml_attr(media_info, 'duration')
|
||||||
|
progress = self.get_xml_attr(session, 'viewOffset')
|
||||||
|
|
||||||
|
session_output = {'sessionKey': self.get_xml_attr(session, 'sessionKey'),
|
||||||
|
'parentThumb': self.get_xml_attr(session, 'parentThumb'),
|
||||||
|
'thumb': self.get_xml_attr(session, 'thumb'),
|
||||||
|
'user': self.get_xml_attr(session.getElementsByTagName('User')[0], 'title'),
|
||||||
|
'player': self.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||||
|
'state': self.get_xml_attr(session.getElementsByTagName('Player')[0], 'state'),
|
||||||
|
'artist': self.get_xml_attr(session, 'grandparentTitle'),
|
||||||
|
'album': self.get_xml_attr(session, 'parentTitle'),
|
||||||
|
'track': self.get_xml_attr(session, 'title'),
|
||||||
|
'ratingKey': self.get_xml_attr(session, 'ratingKey'),
|
||||||
|
'audioDecision': audio_decision,
|
||||||
|
'audioChannels': audio_channels,
|
||||||
|
'audioCodec': audio_codec,
|
||||||
|
'duration': duration,
|
||||||
|
'progress': progress,
|
||||||
|
'progressPercent': str(helpers.get_percent(progress, duration)),
|
||||||
|
'type': 'track'
|
||||||
|
}
|
||||||
|
elif stream_type == 'video':
|
||||||
|
if session.getElementsByTagName('TranscodeSession'):
|
||||||
|
transcode_session = session.getElementsByTagName('TranscodeSession')[0]
|
||||||
|
audio_decision = self.get_xml_attr(transcode_session, 'audioDecision')
|
||||||
|
audio_channels = self.get_xml_attr(transcode_session, 'audioChannels')
|
||||||
|
audio_codec = self.get_xml_attr(transcode_session, 'audioCodec')
|
||||||
|
video_decision = self.get_xml_attr(transcode_session, 'videoDecision')
|
||||||
|
video_codec = self.get_xml_attr(transcode_session, 'videoCodec')
|
||||||
|
width = self.get_xml_attr(transcode_session, 'width')
|
||||||
|
height = self.get_xml_attr(transcode_session, 'height')
|
||||||
|
duration = self.get_xml_attr(session, 'duration')
|
||||||
|
progress = self.get_xml_attr(session, 'viewOffset')
|
||||||
|
else:
|
||||||
|
media_info = session.getElementsByTagName('Media')[0]
|
||||||
|
audio_decision = 'direct play'
|
||||||
|
audio_channels = self.get_xml_attr(media_info, 'audioChannels')
|
||||||
|
audio_codec = self.get_xml_attr(media_info, 'audioCodec')
|
||||||
|
video_decision = 'direct play'
|
||||||
|
video_codec = self.get_xml_attr(media_info, 'videoCodec')
|
||||||
|
width = self.get_xml_attr(media_info, 'width')
|
||||||
|
height = self.get_xml_attr(media_info, 'height')
|
||||||
|
duration = self.get_xml_attr(media_info, 'duration')
|
||||||
|
progress = self.get_xml_attr(session, 'viewOffset')
|
||||||
|
|
||||||
|
if self.get_xml_attr(session, 'type') == 'episode':
|
||||||
|
session_output = {'sessionKey': self.get_xml_attr(session, 'sessionKey'),
|
||||||
|
'art': self.get_xml_attr(session, 'art'),
|
||||||
|
'thumb': self.get_xml_attr(session, 'thumb'),
|
||||||
|
'user': self.get_xml_attr(session.getElementsByTagName('User')[0], 'title'),
|
||||||
|
'player': self.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||||
|
'state': self.get_xml_attr(session.getElementsByTagName('Player')[0], 'state'),
|
||||||
|
'grandparentTitle': self.get_xml_attr(session, 'grandparentTitle'),
|
||||||
|
'title': self.get_xml_attr(session, 'title'),
|
||||||
|
'ratingKey': self.get_xml_attr(session, 'ratingKey'),
|
||||||
|
'audioDecision': audio_decision,
|
||||||
|
'audioChannels': audio_channels,
|
||||||
|
'audioCodec': audio_codec,
|
||||||
|
'videoDecision': video_decision,
|
||||||
|
'videoCodec': video_codec,
|
||||||
|
'height': height,
|
||||||
|
'width': width,
|
||||||
|
'duration': duration,
|
||||||
|
'progress': progress,
|
||||||
|
'progressPercent': str(helpers.get_percent(progress, duration)),
|
||||||
|
'type': self.get_xml_attr(session, 'type')
|
||||||
|
}
|
||||||
|
elif self.get_xml_attr(session, 'type') == 'movie':
|
||||||
|
session_output = {'sessionKey': self.get_xml_attr(session, 'sessionKey'),
|
||||||
|
'art': self.get_xml_attr(session, 'art'),
|
||||||
|
'thumb': self.get_xml_attr(session, 'thumb'),
|
||||||
|
'user': self.get_xml_attr(session.getElementsByTagName('User')[0], 'title'),
|
||||||
|
'player': self.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
|
||||||
|
'state': self.get_xml_attr(session.getElementsByTagName('Player')[0], 'state'),
|
||||||
|
'title': self.get_xml_attr(session, 'title'),
|
||||||
|
'ratingKey': self.get_xml_attr(session, 'ratingKey'),
|
||||||
|
'audioDecision': audio_decision,
|
||||||
|
'audioChannels': audio_channels,
|
||||||
|
'audioCodec': audio_codec,
|
||||||
|
'videoDecision': video_decision,
|
||||||
|
'videoCodec': video_codec,
|
||||||
|
'height': height,
|
||||||
|
'width': width,
|
||||||
|
'duration': duration,
|
||||||
|
'progress': progress,
|
||||||
|
'progressPercent': str(helpers.get_percent(progress, duration)),
|
||||||
|
'type': self.get_xml_attr(session, 'type')
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
logger.warn(u"No known stream types found in session list.")
|
||||||
|
|
||||||
|
return session_output
|
||||||
|
|
||||||
|
"""
|
||||||
|
Return image data as array.
|
||||||
|
Array contains the image content type and image binary
|
||||||
|
|
||||||
|
Parameters required: img { Plex image location }
|
||||||
|
Optional parameters: width { the image width }
|
||||||
|
height { the image height }
|
||||||
|
Output: array
|
||||||
|
"""
|
||||||
|
def get_image(self, img, width='0', height='0'):
|
||||||
|
if img != '':
|
||||||
|
try:
|
||||||
|
http_handler = HTTPConnection(self.host, self.port, timeout=10)
|
||||||
|
if width != '0' and height != '0':
|
||||||
|
image_path = '/photo/:/transcode?url=http://127.0.0.1:' + self.port + img + '&width=' + width + '&height=' + height
|
||||||
|
else:
|
||||||
|
image_path = '/photo/:/transcode?url=http://127.0.0.1:' + self.port + img
|
||||||
|
http_handler.request("GET", image_path + '&X-Plex-Token=' + self.token)
|
||||||
|
response = http_handler.getresponse()
|
||||||
|
request_status = response.status
|
||||||
|
request_content = response.read()
|
||||||
|
request_content_type = response.getheader('content-type')
|
||||||
|
logger.debug(u"Content type: %r" % request_content_type)
|
||||||
|
except IOError, e:
|
||||||
|
logger.warn(u"Failed to retrieve image. %s" % e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if request_status == 200:
|
||||||
|
return [request_content_type, request_content]
|
||||||
|
else:
|
||||||
|
logger.warn(u"Failed to retrieve image. Status code %r" % request_status)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return None
|
|
@ -13,7 +13,7 @@
|
||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from plexpy import logger, db, helpers, notifiers
|
from plexpy import logger, db, helpers, notifiers, plextv, pmsconnect
|
||||||
from plexpy.helpers import checked, radio, today, cleanName
|
from plexpy.helpers import checked, radio, today, cleanName
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
|
||||||
|
@ -161,11 +161,22 @@ class WebInterface(object):
|
||||||
interface_list = [name for name in os.listdir(interface_dir) if
|
interface_list = [name for name in os.listdir(interface_dir) if
|
||||||
os.path.isdir(os.path.join(interface_dir, name))]
|
os.path.isdir(os.path.join(interface_dir, name))]
|
||||||
|
|
||||||
|
# Initialise blank passwords so we do not expose them in the html forms
|
||||||
|
# but users are still able to clear them
|
||||||
|
if plexpy.CONFIG.HTTP_PASSWORD != '':
|
||||||
|
http_password = ' '
|
||||||
|
else:
|
||||||
|
http_password = ''
|
||||||
|
if plexpy.CONFIG.PMS_PASSWORD != '':
|
||||||
|
pms_password = ' '
|
||||||
|
else:
|
||||||
|
pms_password = ''
|
||||||
|
|
||||||
config = {
|
config = {
|
||||||
"http_host": plexpy.CONFIG.HTTP_HOST,
|
"http_host": plexpy.CONFIG.HTTP_HOST,
|
||||||
"http_username": plexpy.CONFIG.HTTP_USERNAME,
|
"http_username": plexpy.CONFIG.HTTP_USERNAME,
|
||||||
"http_port": plexpy.CONFIG.HTTP_PORT,
|
"http_port": plexpy.CONFIG.HTTP_PORT,
|
||||||
"http_password": plexpy.CONFIG.HTTP_PASSWORD,
|
"http_password": http_password,
|
||||||
"launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER),
|
"launch_browser": checked(plexpy.CONFIG.LAUNCH_BROWSER),
|
||||||
"enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS),
|
"enable_https": checked(plexpy.CONFIG.ENABLE_HTTPS),
|
||||||
"https_cert": plexpy.CONFIG.HTTPS_CERT,
|
"https_cert": plexpy.CONFIG.HTTPS_CERT,
|
||||||
|
@ -228,7 +239,7 @@ class WebInterface(object):
|
||||||
"pms_ip": plexpy.CONFIG.PMS_IP,
|
"pms_ip": plexpy.CONFIG.PMS_IP,
|
||||||
"pms_port": plexpy.CONFIG.PMS_PORT,
|
"pms_port": plexpy.CONFIG.PMS_PORT,
|
||||||
"pms_username": plexpy.CONFIG.PMS_USERNAME,
|
"pms_username": plexpy.CONFIG.PMS_USERNAME,
|
||||||
"pms_password": plexpy.CONFIG.PMS_PASSWORD,
|
"pms_password": pms_password,
|
||||||
"plexwatch_database": plexpy.CONFIG.PLEXWATCH_DATABASE,
|
"plexwatch_database": plexpy.CONFIG.PLEXWATCH_DATABASE,
|
||||||
"date_format": plexpy.CONFIG.DATE_FORMAT,
|
"date_format": plexpy.CONFIG.DATE_FORMAT,
|
||||||
"time_format": plexpy.CONFIG.TIME_FORMAT,
|
"time_format": plexpy.CONFIG.TIME_FORMAT,
|
||||||
|
@ -257,6 +268,30 @@ class WebInterface(object):
|
||||||
# checked items should be zero or one. if they were not sent then the item was not checked
|
# checked items should be zero or one. if they were not sent then the item was not checked
|
||||||
kwargs[checked_config] = 0
|
kwargs[checked_config] = 0
|
||||||
|
|
||||||
|
# Write Plex token to the config
|
||||||
|
if (not plexpy.CONFIG.PMS_TOKEN or plexpy.CONFIG.PMS_TOKEN == '' \
|
||||||
|
or kwargs['pms_username'] != plexpy.CONFIG.PMS_USERNAME) \
|
||||||
|
and (kwargs['pms_username'] != '' or kwargs['pms_password'] != ''):
|
||||||
|
|
||||||
|
plex_tv = plextv.PlexTV(kwargs['pms_username'], kwargs['pms_password'])
|
||||||
|
token = plex_tv.get_token()
|
||||||
|
|
||||||
|
if token:
|
||||||
|
kwargs['pms_token'] = token
|
||||||
|
logger.info('Plex.tv token sucessfully written to config.')
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to write Plex.tv token to config.')
|
||||||
|
|
||||||
|
# Clear Plex token if username or password set to blank
|
||||||
|
if kwargs['pms_username'] == '' or kwargs['pms_password'] == '':
|
||||||
|
kwargs['pms_token'] = ''
|
||||||
|
|
||||||
|
# If passwords exists in config, do not overwrite when blank value received
|
||||||
|
if kwargs['http_password'] == ' ' and plexpy.CONFIG.HTTP_PASSWORD != '':
|
||||||
|
kwargs['http_password'] = plexpy.CONFIG.HTTP_PASSWORD
|
||||||
|
if kwargs['pms_password'] == ' ' and plexpy.CONFIG.PMS_PASSWORD != '':
|
||||||
|
kwargs['pms_password'] = plexpy.CONFIG.PMS_PASSWORD
|
||||||
|
|
||||||
for plain_config, use_config in [(x[4:], x) for x in kwargs if x.startswith('use_')]:
|
for plain_config, use_config in [(x[4:], x) for x in kwargs if x.startswith('use_')]:
|
||||||
# the use prefix is fairly nice in the html, but does not match the actual config
|
# the use prefix is fairly nice in the html, but does not match the actual config
|
||||||
kwargs[plain_config] = kwargs[use_config]
|
kwargs[plain_config] = kwargs[use_config]
|
||||||
|
@ -325,7 +360,7 @@ class WebInterface(object):
|
||||||
sortcolumn = 'duration'
|
sortcolumn = 'duration'
|
||||||
|
|
||||||
if search_value == "":
|
if search_value == "":
|
||||||
query = 'SELECT id, time, user, platform, ip_address, title, time, paused_counter, stopped, xml, \
|
query = 'SELECT id, time, user, platform, ip_address, title, time, paused_counter, stopped, ratingKey, xml, \
|
||||||
round((julianday(datetime(stopped, "unixepoch", "localtime")) - \
|
round((julianday(datetime(stopped, "unixepoch", "localtime")) - \
|
||||||
julianday(datetime(time, "unixepoch", "localtime"))) * 86400) - \
|
julianday(datetime(time, "unixepoch", "localtime"))) * 86400) - \
|
||||||
(case when paused_counter is null then 0 else paused_counter end) as duration \
|
(case when paused_counter is null then 0 else paused_counter end) as duration \
|
||||||
|
@ -333,7 +368,7 @@ class WebInterface(object):
|
||||||
filtered = myDB.select(query)
|
filtered = myDB.select(query)
|
||||||
totalcount = len(filtered)
|
totalcount = len(filtered)
|
||||||
else:
|
else:
|
||||||
query = 'SELECT id, time, user, platform, ip_address, title, time, paused_counter, stopped, xml, \
|
query = 'SELECT id, time, user, platform, ip_address, title, time, paused_counter, stopped, ratingKey, xml, \
|
||||||
round((julianday(datetime(stopped, "unixepoch", "localtime")) - \
|
round((julianday(datetime(stopped, "unixepoch", "localtime")) - \
|
||||||
julianday(datetime(time, "unixepoch", "localtime"))) * 86400) - \
|
julianday(datetime(time, "unixepoch", "localtime"))) * 86400) - \
|
||||||
(case when paused_counter is null then 0 else paused_counter end) as duration \
|
(case when paused_counter is null then 0 else paused_counter end) as duration \
|
||||||
|
@ -354,6 +389,7 @@ class WebInterface(object):
|
||||||
"started": item["time"],
|
"started": item["time"],
|
||||||
"paused": item["paused_counter"],
|
"paused": item["paused_counter"],
|
||||||
"stopped": item["stopped"],
|
"stopped": item["stopped"],
|
||||||
|
"rating_key": item["ratingKey"],
|
||||||
"duration": item["duration"],
|
"duration": item["duration"],
|
||||||
"percent_complete": 0,
|
"percent_complete": 0,
|
||||||
}
|
}
|
||||||
|
@ -486,3 +522,111 @@ class WebInterface(object):
|
||||||
logger.warn(msg)
|
logger.warn(msg)
|
||||||
return msg
|
return msg
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_pms_token(self):
|
||||||
|
|
||||||
|
token = plextv.PlexTV()
|
||||||
|
result = token.get_token()
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to retrieve Plex.tv token.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_pms_sessions_json(self, **kwargs):
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_sessions('json')
|
||||||
|
|
||||||
|
if result:
|
||||||
|
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_current_activity(self, **kwargs):
|
||||||
|
|
||||||
|
try:
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_current_activity()
|
||||||
|
except:
|
||||||
|
return serve_template(templatename="current_activity.html", activity=None)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return serve_template(templatename="current_activity.html", activity=result)
|
||||||
|
else:
|
||||||
|
return serve_template(templatename="current_activity.html", activity=None)
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_current_activity_header(self, **kwargs):
|
||||||
|
|
||||||
|
try:
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_current_activity()
|
||||||
|
except IOError, e:
|
||||||
|
return serve_template(templatename="current_activity_header.html", activity=None)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return serve_template(templatename="current_activity_header.html", activity=result['stream_count'])
|
||||||
|
else:
|
||||||
|
return serve_template(templatename="current_activity_header.html", activity=None)
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def pms_image_proxy(self, img='', width='0', height='0', **kwargs):
|
||||||
|
if img != '':
|
||||||
|
try:
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_image(img, width, height)
|
||||||
|
logger.info('Image proxy queried. Content type is %s' % result[0])
|
||||||
|
cherrypy.response.headers['Content-type'] = result[0]
|
||||||
|
return result[1]
|
||||||
|
except:
|
||||||
|
logger.warn('Image proxy queried but errors occured.')
|
||||||
|
return 'No image'
|
||||||
|
else:
|
||||||
|
logger.warn('Image proxy queried but no parameters received.')
|
||||||
|
return 'No image'
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def info(self, rating_key='', **kwargs):
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_metadata_details(rating_key)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
return serve_template(templatename="info.html", metadata=result['metadata'], title="Info")
|
||||||
|
else:
|
||||||
|
return serve_template(templatename="info.html", metadata='', title="Info")
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_metadata_json(self, rating_key='', **kwargs):
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_metadata(rating_key, 'json')
|
||||||
|
|
||||||
|
if result:
|
||||||
|
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_metadata_xml(self, rating_key='', **kwargs):
|
||||||
|
|
||||||
|
pms_connect = pmsconnect.PmsConnect()
|
||||||
|
result = pms_connect.get_metadata(rating_key)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
cherrypy.response.headers['Content-type'] = 'application/xml'
|
||||||
|
return result
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue