mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-08-19 21:03:21 -07:00
Merge branch 'dev'
This commit is contained in:
commit
97c414d1ad
42 changed files with 4562 additions and 581 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -23,6 +23,9 @@ cache/*
|
|||
*.csr
|
||||
*.pem
|
||||
|
||||
# Mergetool
|
||||
*.orgin
|
||||
|
||||
# OS generated files #
|
||||
######################
|
||||
.DS_Store?
|
||||
|
@ -32,7 +35,7 @@ Icon?
|
|||
Thumbs.db
|
||||
|
||||
#Ignore files generated by PyCharm
|
||||
.idea/*
|
||||
*.idea/*
|
||||
|
||||
#Ignore files generated by vi
|
||||
*.swp
|
||||
|
|
28
CHANGELOG.md
28
CHANGELOG.md
|
@ -1,5 +1,33 @@
|
|||
# Changelog
|
||||
|
||||
## v1.3.7 (2016-02-20)
|
||||
|
||||
* Fix: Verifying server with SSL enabled.
|
||||
* Fix: Regression where {stream_duration} reported as 0.
|
||||
* Fix: Video metadata flags showing up for track info.
|
||||
* Fix: Custom library icons not applied to Library Statistics.
|
||||
* Fix: Typos in the Web UI.
|
||||
* Add: ETA to Current Activity overlay.
|
||||
* Add: Total duration to Libraries and Users tables.
|
||||
* Add: {machine_id} to notification options.
|
||||
* Add: IMDB, TVDB, TMDb, Last.fm, and Trackt IDs/URLs to notification options.
|
||||
* Add: {poster_url} to notification options using Imgur.
|
||||
* Add: Poster and link for Facebook notifications.
|
||||
* Add: Log javascript errors from the Web UI.
|
||||
* Add: Configuration and Scheduler info to the settings page.
|
||||
* Add: Schedule background task to backup the PlexPy database.
|
||||
* Add: URL anonymizer for external links.
|
||||
* Add: Plex Media Scanner log file to Log viewer.
|
||||
* Add: API v2 (sill very experimental) (Thanks @Hellowlol)
|
||||
* Change: Allow secure websocket connections.
|
||||
* Change: History grouping now accounts for the view offset.
|
||||
* Change: Subject line can be toggled off for Facebook, Slack, Telegram, and Twitter.
|
||||
* Change: Create self-signed SSL certificates when enabling HTTPS.
|
||||
* Change: Revert homepage "Last Played" to "Last Watched".
|
||||
* Change: Disable monitor remote access checkbox if remote access is not enabled on the PMS.
|
||||
* Change: Disable IP logging checkbox if PMS version is 0.9.14 or greater.
|
||||
|
||||
|
||||
## v1.3.6 (2016-02-03)
|
||||
|
||||
* Fix: Regression where {duration} not reported in minutes.
|
||||
|
|
|
@ -501,7 +501,8 @@ textarea.form-control:focus {
|
|||
.libraries-poster-face {
|
||||
overflow: hidden;
|
||||
float: left;
|
||||
background-size: contain;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
|
@ -1717,7 +1718,8 @@ a:hover .item-children-poster {
|
|||
float: left;
|
||||
margin-top: 15px;
|
||||
margin-right: 15px;
|
||||
background-size: contain;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
/*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
|
||||
|
@ -2178,6 +2180,10 @@ a .home-platforms-instance-list-oval:hover,
|
|||
.refresh-libraries-button {
|
||||
float: right;
|
||||
}
|
||||
.refresh-users-button,
|
||||
.refresh-libraries-button {
|
||||
margin-right: 5px;
|
||||
}
|
||||
.nav-settings,
|
||||
.nav-settings ul {
|
||||
margin: 0px 0px 20px 0px;
|
||||
|
@ -2712,4 +2718,44 @@ table[id^='media_info_child'] table[id^='media_info_child'] thead th {
|
|||
}
|
||||
.selectize-input input[type='text'] {
|
||||
height: 20px;
|
||||
}
|
||||
.small-muted {
|
||||
font-size: small;
|
||||
color: #777;
|
||||
}
|
||||
.config-info-table,
|
||||
.config-scheduler-table {
|
||||
width: 100%
|
||||
}
|
||||
.config-info-table td,
|
||||
.config-info-table th,
|
||||
.config-scheduler-table td,
|
||||
.config-scheduler-table th {
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
.config-info-table td:first-child {
|
||||
width: 150px;
|
||||
}
|
||||
.config-scheduler-table td:first-child {
|
||||
width: 225px;
|
||||
}
|
||||
.config-scheduler-table th {
|
||||
color: #fff;
|
||||
}
|
||||
a.no-highlight {
|
||||
color: #777;
|
||||
}
|
||||
a.no-highlight:hover {
|
||||
color: #fff;
|
||||
}
|
||||
.top-line {
|
||||
border-top: 1px dotted #777;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.help-bold {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
.save-button {
|
||||
margin-top: 15px;
|
||||
}
|
|
@ -198,6 +198,13 @@ DOCUMENTATION :: END
|
|||
% else:
|
||||
<span>IP: N/A</span>
|
||||
% endif
|
||||
<br />
|
||||
ETA:
|
||||
<span id="stream-eta-${a['session_key']}">
|
||||
<script>
|
||||
$("#stream-eta-${a['session_key']}").html(moment().add(parseInt(${a['duration']}) - parseInt(${a['view_offset']}), 'milliseconds').format(time_format));
|
||||
</script>
|
||||
</span>
|
||||
</div>
|
||||
<div class="dashboard-activity-poster-info-time">
|
||||
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
|
||||
|
|
|
@ -692,7 +692,7 @@ DOCUMENTATION :: END
|
|||
<li>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Last Played</h4>
|
||||
<h4>Last Watched</h4>
|
||||
</div>
|
||||
<div class="home-platforms-instance-last-user">
|
||||
<h4>
|
||||
|
|
|
@ -171,10 +171,10 @@ DOCUMENTATION :: END
|
|||
% endif
|
||||
% if data['media_type'] == 'movie' or data['media_type'] == 'episode' or data['media_type'] == 'track':
|
||||
<div class="summary-content-media-info-wrapper">
|
||||
% if data['video_codec']:
|
||||
% if data['media_type'] != 'track' and data['video_codec']:
|
||||
<img class="summary-content-media-flag" title="${data['video_codec']}" src="interfaces/default/images/media_flags/video_codec/${data['video_codec'] | vf}.png" />
|
||||
% endif
|
||||
% if data['video_resolution']:
|
||||
% if data['media_type'] != 'track' and data['video_resolution']:
|
||||
<img class="summary-content-media-flag" title="${data['video_resolution']}" src="interfaces/default/images/media_flags/video_resolution/${data['video_resolution']}.png" />
|
||||
% endif
|
||||
% if data['audio_codec']:
|
||||
|
|
|
@ -54,7 +54,7 @@ function showMsg(msg,loader,timeout,ms,error) {
|
|||
}
|
||||
}
|
||||
|
||||
function doAjaxCall(url,elem,reload,form) {
|
||||
function doAjaxCall(url, elem, reload, form, callback) {
|
||||
// Set Message
|
||||
feedback = $("#ajaxMsg");
|
||||
update = $("#updatebar");
|
||||
|
@ -157,6 +157,9 @@ function doAjaxCall(url,elem,reload,form) {
|
|||
complete: function(jqXHR, textStatus) {
|
||||
// Remove loaders and stuff, ajax request is complete!
|
||||
loader.remove();
|
||||
if (typeof callback === "function") {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -252,13 +255,13 @@ function isPrivateIP(ip_address) {
|
|||
|
||||
function humanTime(seconds) {
|
||||
if (seconds >= 86400) {
|
||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) +
|
||||
'</h3><p> days </p><h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) +
|
||||
'</h3><p> hrs</p><h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
text = '<h3>' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '</h3><p> days</p>' +
|
||||
'<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' +
|
||||
'<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
return text;
|
||||
} else if (seconds >= 3600) {
|
||||
text = '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) +
|
||||
'</h3><p>hrs</p><h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
text = '<h3>' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '</h3><p> hrs</p>' +
|
||||
'<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
return text;
|
||||
} else if (seconds >= 60) {
|
||||
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
|
@ -269,6 +272,25 @@ function humanTime(seconds) {
|
|||
}
|
||||
}
|
||||
|
||||
function humanTimeClean(seconds) {
|
||||
if (seconds >= 86400) {
|
||||
text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' +
|
||||
Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' +
|
||||
Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||
return text;
|
||||
} else if (seconds >= 3600) {
|
||||
text = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' +
|
||||
Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||
return text;
|
||||
} else if (seconds >= 60) {
|
||||
text = Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins';
|
||||
return text;
|
||||
} else {
|
||||
text = '0';
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
String.prototype.toProperCase = function () {
|
||||
return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();});
|
||||
};
|
||||
|
@ -372,3 +394,16 @@ function clearSearchButton(tableName, table) {
|
|||
table.search('').draw();
|
||||
});
|
||||
}
|
||||
|
||||
// Taken from https://github.com/Hellowlol/HTPC-Manager
|
||||
window.onerror = function (message, file, line) {
|
||||
var e = {
|
||||
'page': window.location.href,
|
||||
'message': message,
|
||||
'file': file,
|
||||
'line': line
|
||||
};
|
||||
|
||||
$.post("log_js_errors", e, function (data) {
|
||||
});
|
||||
};
|
|
@ -161,12 +161,28 @@ libraries_list_table_options = {
|
|||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "25%",
|
||||
"width": "18%",
|
||||
"className": "hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [9],
|
||||
"data": "plays",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "7%"
|
||||
},
|
||||
{
|
||||
"targets": [10],
|
||||
"data": "duration",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html(humanTimeClean(cellData));
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "10%"
|
||||
}
|
||||
|
|
|
@ -165,12 +165,28 @@ users_list_table_options = {
|
|||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "30%",
|
||||
"width": "23%",
|
||||
"className": "hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [8],
|
||||
"data": "plays",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "7%"
|
||||
},
|
||||
{
|
||||
"targets": [9],
|
||||
"data": "duration",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== null && cellData !== '') {
|
||||
$(td).html(humanTimeClean(cellData));
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "10%"
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<%def name="headIncludes()">
|
||||
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/dataTables.colVis.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
|
||||
</%def>
|
||||
|
||||
|
@ -23,6 +24,7 @@
|
|||
<span><i class="fa fa-book"></i> All Libraries</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="colvis-button-bar hidden-xs"></div>
|
||||
% if config['update_section_ids'] == -1:
|
||||
<button class="btn btn-dark refresh-libraries-button" id="refresh-libraries-list" disabled><i class="fa fa-refresh"></i> Refresh libraries</button>
|
||||
% else:
|
||||
|
@ -48,6 +50,7 @@
|
|||
<th align="left" id="last_accessed">Last Accessed</th>
|
||||
<th align="left" id="last_played">Last Played</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
<th align="left" id="total_duration">Total Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -79,6 +82,7 @@
|
|||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
||||
<script src="interfaces/default/js/dataTables.colVis.js"></script>
|
||||
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
|
@ -96,6 +100,8 @@
|
|||
}
|
||||
|
||||
libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('libraries_list_table', libraries_list_table);
|
||||
|
||||
|
|
|
@ -37,7 +37,9 @@ DOCUMENTATION :: END
|
|||
% if data:
|
||||
<div class="container-fluid">
|
||||
<div class="row">
|
||||
% if data['library_art']:
|
||||
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['library_art']}&width=1920&height=1080)"></div>
|
||||
% endif
|
||||
<div class="summary-container">
|
||||
<div class="summary-navbar">
|
||||
<div class="col-md-12">
|
||||
|
@ -52,7 +54,7 @@ DOCUMENTATION :: END
|
|||
<div class="col-md-12">
|
||||
<div class="table-card-back">
|
||||
<div class="user-info-wrapper">
|
||||
% if data['library_thumb'][:4] == 'http':
|
||||
% if data['library_thumb'][:4] == 'http' or data['library_thumb'][:10] == 'interfaces':
|
||||
<div class="library-info-poster-face" style="background-image: url(${data['library_thumb']});"></div>
|
||||
% else:
|
||||
<div class="library-info-poster-face" style="background-image: url(pms_image_proxy?img=${data['library_thumb']}&width=80&height=80&fallback=cover);"></div>
|
||||
|
|
|
@ -91,6 +91,6 @@ DOCUMENTATION :: END
|
|||
</ul>
|
||||
</div>
|
||||
% else:
|
||||
<div class="text-muted">Unable to retrieve data from database.
|
||||
<div class="text-muted">No stats to show.
|
||||
</div><br>
|
||||
% endif
|
|
@ -75,13 +75,13 @@ DOCUMENTATION :: END
|
|||
</div>
|
||||
% endif
|
||||
</div>
|
||||
% if library['thumb']:
|
||||
% if library['thumb'].startswith("http"):
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="home-platforms-library-thumb" style="background-image: url(pms_image_proxy?img=${library['thumb']}&width=300&height=300&fallback=poster);"></div>
|
||||
<div class="home-platforms-library-thumb" style="background-image: url(${library['thumb']});"></div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="home-platforms-library-thumb" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||
<div class="home-platforms-library-thumb" style="background-image: url(pms_image_proxy?img=${library['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
</div>
|
||||
% endif
|
||||
</li>
|
||||
|
|
|
@ -29,6 +29,7 @@ from plexpy import helpers
|
|||
<ul class="nav nav-pills" role="tablist">
|
||||
<li role="presentation" class="active"><a id="plexpy-logs-btn" href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">PlexPy Logs</a></li>
|
||||
<li role="presentation"><a id="plex-logs-btn" href="#tabs-2" aria-controls="tabs-2" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
|
||||
<li role="presentation"><a id="plex-scanner-logs-btn" href="#tabs-3" aria-controls="tabs-3" role="tab" data-toggle="tab">Plex Media Scanner Logs</a></li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
||||
|
@ -57,6 +58,19 @@ from plexpy import helpers
|
|||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-3">
|
||||
<table class="display" id="plex_scanner_log_table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align='left' id="plex_scanner_timestamp">Timestamp</th>
|
||||
<th align='left' id="plex_scanner_level">Level</th>
|
||||
<th align='left' id="plex_scanner_message">Message</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -98,11 +112,18 @@ from plexpy import helpers
|
|||
|
||||
function LoadPlexLogs() {
|
||||
plex_log_table_options.ajax = {
|
||||
"url": "get_plex_log"
|
||||
"url": "get_plex_log?log_type=server"
|
||||
}
|
||||
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
|
||||
}
|
||||
|
||||
function LoadPlexScannerLogs() {
|
||||
plex_log_table_options.ajax = {
|
||||
"url": "get_plex_log?log_type=scanner"
|
||||
}
|
||||
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
|
||||
}
|
||||
|
||||
$("#plexpy-logs-btn").click(function() {
|
||||
$("#clear-logs").show();
|
||||
LoadPlexPyLogs();
|
||||
|
@ -115,6 +136,12 @@ from plexpy import helpers
|
|||
clearSearchButton('plex_log_table', plex_log_table);
|
||||
});
|
||||
|
||||
$("#plex-scanner-logs-btn").click(function() {
|
||||
$("#clear-logs").hide();
|
||||
LoadPlexScannerLogs();
|
||||
clearSearchButton('plex_scanner_log_table', plex_scanner_log_table);
|
||||
});
|
||||
|
||||
$("#clear-logs").click(function() {
|
||||
var r = confirm("Are you sure you want to clear the PlexPy log?");
|
||||
if (r == true) {
|
||||
|
|
|
@ -132,7 +132,7 @@ from plexpy import helpers
|
|||
function reloadModal() {
|
||||
$.ajax({
|
||||
url: 'get_notification_agent_config',
|
||||
data: { config_id: '${agent["id"]}' },
|
||||
data: { agent_id: '${agent["id"]}' },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
|
@ -147,9 +147,8 @@ from plexpy import helpers
|
|||
})
|
||||
|
||||
$('#save-notification-item').click(function () {
|
||||
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
||||
// Reload modal to update certain fields
|
||||
reloadModal();
|
||||
doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal);
|
||||
return false;
|
||||
});
|
||||
|
||||
|
@ -195,7 +194,7 @@ from plexpy import helpers
|
|||
$.ajax({
|
||||
url: 'test_notifier',
|
||||
data: {
|
||||
config_id: '${agent["id"]}',
|
||||
agent_id: '${agent["id"]}',
|
||||
subject: $('#test_subject').val(),
|
||||
body: $('#test_body').val(),
|
||||
script: $('#test_script').val(),
|
||||
|
@ -211,8 +210,8 @@ from plexpy import helpers
|
|||
});
|
||||
|
||||
$('#pushbullet_apikey, #pushover_apitoken, #scripts_folder').on('change', function () {
|
||||
doAjaxCall('set_notification_config', $(this), 'tabs', true);
|
||||
reloadModal();
|
||||
// Reload modal to update certain fields
|
||||
doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal);
|
||||
return false;
|
||||
});
|
||||
|
||||
|
|
64
data/interfaces/default/scheduler_table.html
Normal file
64
data/interfaces/default/scheduler_table.html
Normal file
|
@ -0,0 +1,64 @@
|
|||
<%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: scheduler_table.html
|
||||
Version: 0.1
|
||||
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<%!
|
||||
import arrow
|
||||
import plexpy
|
||||
from plexpy import common
|
||||
|
||||
scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
|
||||
%>
|
||||
|
||||
<table class="config-scheduler-table small-muted">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Scheduled Task</th>
|
||||
<th>State</th>
|
||||
<th>Interval</th>
|
||||
<th>Next Run In</th>
|
||||
<th>Next Run Time</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
% for job in common.SCHEDULER_LIST:
|
||||
% if job in scheduled_jobs:
|
||||
<%
|
||||
sched_job = plexpy.SCHED.get_job(job)
|
||||
run_interval = arrow.get(str(sched_job.trigger.interval), ['H:mm:ss', 'HH:mm:ss'])
|
||||
next_run_interval = arrow.get(sched_job.next_run_time).timestamp - arrow.now().timestamp
|
||||
%>
|
||||
<tr>
|
||||
<td>${sched_job.id}</td>
|
||||
<td><i class="fa fa-sm fa-fw fa-check"></i> Active</td>
|
||||
<td>${arrow.get(run_interval).format('HH:mm:ss')}</td>
|
||||
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td>
|
||||
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td>
|
||||
</tr>
|
||||
% elif job == 'Check for active sessions' and plexpy.CONFIG.MONITORING_USE_WEBSOCKET and not plexpy.POLLING_FAILOVER:
|
||||
<tr>
|
||||
<td>${job}</td>
|
||||
<td><i class="fa fa-sm fa-fw fa-check"></i> Using Websocket</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
</tr>
|
||||
% else:
|
||||
<tr>
|
||||
<td>${job}</td>
|
||||
<td><i class="fa fa-sm fa-fw fa-times"></i> Inactive</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
<td>N/A</td>
|
||||
</tr>
|
||||
% endif
|
||||
% endfor
|
||||
</tbody>
|
||||
</table>
|
|
@ -1,7 +1,9 @@
|
|||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
import sys
|
||||
import plexpy
|
||||
from plexpy import notifiers, common, versioncheck
|
||||
from plexpy.helpers import anon_url
|
||||
|
||||
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['name'])
|
||||
%>
|
||||
|
@ -33,7 +35,8 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<!-- Nav tabs -->
|
||||
<div class="col-md-3">
|
||||
<ul class="nav-settings list-unstyled" role="tablist">
|
||||
<li role="presentation" class="active"><a href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">General</a></li>
|
||||
<li role="presentation" class="active"><a href="#tabs-0" aria-controls="tabs-0" role="tab" data-toggle="tab">Help & Info</a></li>
|
||||
<li role="presentation"><a href="#tabs-1" aria-controls="tabs-1" role="tab" data-toggle="tab">General</a></li>
|
||||
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" role="tab" data-toggle="tab">Homepage Statistics</a></li>
|
||||
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" role="tab" data-toggle="tab">Web Interface</a></li>
|
||||
<li role="presentation"><a href="#tabs-4" aria-controls="tabs-4" role="tab" data-toggle="tab">Access Control</a></li>
|
||||
|
@ -48,21 +51,97 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<div class="col-md-9">
|
||||
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-0">
|
||||
% if common.VERSION_NUMBER:
|
||||
<div class="padded-header">
|
||||
<h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
|
||||
</div>
|
||||
% endif
|
||||
<div class="padded-header">
|
||||
<h3>PlexPy Configuration</h3>
|
||||
</div>
|
||||
<table class="config-info-table small-muted">
|
||||
<tbody>
|
||||
% if plexpy.CURRENT_VERSION:
|
||||
<tr>
|
||||
<td>Git Branch:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy/tree/%s' % plexpy.CONFIG.GIT_BRANCH)}">${plexpy.CONFIG.GIT_BRANCH}</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Git Hash:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy/commit/%s' % plexpy.CURRENT_VERSION)}">${plexpy.CURRENT_VERSION}</a></td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td>Configuration File:</td>
|
||||
<td>${plexpy.CONFIG_FILE}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Database File:</td>
|
||||
<td>${plexpy.DB_FILE}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Backup Directory:</td>
|
||||
<td>${plexpy.CONFIG.BACKUP_DIR}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Cache Directory:</td>
|
||||
<td>${plexpy.CONFIG.CACHE_DIR}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Log Directory:</td>
|
||||
<td>${plexpy.CONFIG.LOG_DIR}</td>
|
||||
</tr>
|
||||
% if plexpy.ARGS:
|
||||
<tr>
|
||||
<td>Arguments:</td>
|
||||
<td>${plexpy.ARGS}</td>
|
||||
</tr>
|
||||
% endif
|
||||
<tr>
|
||||
<td>Platform:</td>
|
||||
<td>${common.PLATFORM} ${common.PLATFORM_VERSION}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Python Version:</td>
|
||||
<td>${sys.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="top-line">Plex Forums:</td>
|
||||
<td class="top-line"><a class="no-highlight" href="${anon_url('https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program')}" target="_blank">https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Wiki:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy/wiki')}" target="_blank">https://github.com/drzoidberg33/plexpy/wiki</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Source:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://github.com/drzoidberg33/plexpy')}" target="_blank">https://github.com/drzoidberg33/plexpy</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Gitter Chat:</td>
|
||||
<td><a class="no-highlight" href="${anon_url('https://gitter.im/drzoidberg33/plexpy')}" target="_blank">https://gitter.im/drzoidberg33/plexpy</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="padded-header">
|
||||
<h3>PlexPy Scheduler</h3>
|
||||
</div>
|
||||
<div id="plexpy-scheduler-table">
|
||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading scheduler table...</div>
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-1">
|
||||
<div class="padded-header">
|
||||
<h3>Updates</h3>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
|
||||
</label>
|
||||
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
|
||||
</div>
|
||||
% if plexpy.CURRENT_VERSION:
|
||||
<p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
|
||||
% endif
|
||||
<div class="padded-header">
|
||||
<h3>Display Settings</h3>
|
||||
</div>
|
||||
|
@ -145,7 +224,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<li class="card card-sortable">
|
||||
<div class="card-handle"><i class="fa fa-bars"></i></div>
|
||||
<label>
|
||||
<input type="checkbox" id="hscard-last_watched" name="hscard-last_watched" value="last_watched"> Last Played
|
||||
<input type="checkbox" id="hscard-last_watched" name="hscard-last_watched" value="last_watched"> Last Watched
|
||||
</label>
|
||||
</li>
|
||||
<li class="card card-sortable">
|
||||
|
@ -256,16 +335,51 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<p class="help-block">Enable HTTPS for web server for encrypted communication.</p>
|
||||
</div>
|
||||
<div id="https_options">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" class="http-settings" name="https_create_cert" id="https_create_cert" value="1" ${config['https_create_cert']} /> Create Self-signed Certificate
|
||||
</label>
|
||||
<p class="help-block">Check to have PlexPy create a self-signed SSL certificate. Uncheck if you want to use your own certificate.</p>
|
||||
</div>
|
||||
<div id="https_options_self-signed">
|
||||
<div class="form-group">
|
||||
<label for="https_domain">HTTPS Domains</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control http-settings" id="https_domain" name="https_domain" value="${config['https_domain']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">The domain names used to access PlexPy, separated by commas (,).</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="https_ip">HTTPS IPs</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control http-settings" id="https_ip" name="https_ip" value="${config['https_ip']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">The IP addresses used to access PlexPy, separated by commas (,).</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="https_cert">HTTPS Cert</label>
|
||||
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">The location of the SSL certificate.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="https_key">HTTPS Key</label>
|
||||
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">The location of the SSL key.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane" id="tabs-4">
|
||||
|
@ -355,13 +469,13 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Force SSL
|
||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1" ${config['pms_ssl']}> Use SSL
|
||||
</label>
|
||||
<p class="help-block">Force PlexPy to connect to your Plex Server via SSL. Your server needs to have remote access enabled.</p>
|
||||
<p class="help-block">If you have secure connections enabled on your Plex Server, communicate with it securely.</p>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
|
||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display:none">
|
||||
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Plex Logs</h3>
|
||||
|
@ -375,7 +489,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</div>
|
||||
</div>
|
||||
<p class="help-block">Set the complete folder path where your Plex Server logs are, shortcuts are not recognized.<br />
|
||||
<a href="https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files" target="_blank">Click here</a> for help. This is required if you enable IP logging (for PMS 0.9.12 and below). </p>
|
||||
<a href="${anon_url('https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files')}" target="_blank">Click here</a> for help. This is required if you enable IP logging (for PMS 0.9.12 and below). </p>
|
||||
</div>
|
||||
|
||||
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
|
||||
|
@ -462,6 +576,15 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<p class="help-block">Enable if you want PlexPy to calculate the total file size for TV Shows/Seasons and Artists/Albums on the media info tables.<br />
|
||||
This is currently experimental.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="anon_redirect">Anonymous Redirect</label>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<input type="text" class="form-control" id="anon_redirect" name="anon_redirect" value="${config['anon_redirect']}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Backlink protection via anonymizer service, must end in "?".</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>PlexWatch Import Tool</h3>
|
||||
|
@ -490,13 +613,14 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<input type="checkbox" class="monitor-settings" id="monitoring_use_websocket" name="monitoring_use_websocket" value="1" ${config['monitoring_use_websocket']}> Use Websocket (requires restart) [experimental]
|
||||
</label>
|
||||
<p class="help-block">Instead of polling the server at regular intervals let the server tell us when something happens.<br />
|
||||
This is currently experimental. Encrypted websocket is not currently supported.</p>
|
||||
This is currently experimental.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
|
||||
</label>
|
||||
<p class="help-block">Enable to have PlexPy check if remote access to the Plex Media Server goes down. Your server needs to have remote access enabled.</p>
|
||||
<span id="remoteAccessCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
||||
<p class="help-block">Enable to have PlexPy check if remote access to the Plex Media Server goes down.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
|
@ -534,7 +658,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</label>
|
||||
<span id="debugLogCheck" style="color: #eb8600; padding-left: 10px;"></span>
|
||||
<p class="help-block">
|
||||
Enable this to attempt to log the IP address of the user (for PMS 0.9.12 and below, IP address is automatically logged for PMS 0.9.14 and above).
|
||||
Enable this to attempt to log the IP address of the user.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
@ -639,11 +763,11 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</div>
|
||||
<p class="help-block">
|
||||
You can set custom formatted text for each type of notification.
|
||||
Click <a href="#notify-text-sub-modal" data-toggle="modal">here</a> for a list of available parameters which can be used.
|
||||
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a list of available parameters which can be used.
|
||||
</p>
|
||||
<p class="help-block">
|
||||
You can also add tags to exclude certain text depending on the media type. Click
|
||||
<a href="#notify-text-tags-modal" data-toggle="modal">here</a> to view usage information.
|
||||
You can also add tags to exclude certain text depending on the media type.
|
||||
<a href="#notify-text-tags-modal" data-toggle="modal">Click here</a> to view usage information.
|
||||
</p>
|
||||
<br/>
|
||||
<ul id="accordion-session" class="accordion list-unstyled">
|
||||
|
@ -859,7 +983,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<h3>Notification Agents</h3>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
Toggle the desired notification options by clicking the bell icon and configure it by clicking the settings icon to the right.
|
||||
Toggle the desired notification options by clicking the <span class="help-bold">bell icon (<i class="fa fa-sm fa-bell"></i>)</span> and configure it by clicking the settings icon to the right.
|
||||
</p>
|
||||
<br/>
|
||||
<ul class="stacked-configs list-unstyled">
|
||||
|
@ -1015,8 +1139,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i
|
||||
class="fa fa-remove"></i></button>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||
<h4 class="modal-title">Fetch Plex.tv Token</h4>
|
||||
</div>
|
||||
<div class="modal-body" id="modal-text">
|
||||
|
@ -1128,7 +1251,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</tr>
|
||||
<tr>
|
||||
<td><strong>{ip_address}</strong></td>
|
||||
<td>The IP address of the device being used for playback. (PMS 0.9.14 and above)</td>
|
||||
<td>The IP address of the device being used for playback. <span class="small-muted">(PMS 0.9.14 and above)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{stream_duration}</strong></td>
|
||||
|
@ -1242,6 +1365,10 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<td><strong>{user_id}</strong></td>
|
||||
<td>The unique identifier for the user.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{machine_id}</strong></td>
|
||||
<td>The unique identifier for the player.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="notification-params">
|
||||
|
@ -1255,7 +1382,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<tbody>
|
||||
<tr>
|
||||
<td><strong>{media_type}</strong></td>
|
||||
<td>The type of media (movie, episode, track).</td>
|
||||
<td>The type of media. <span class="small-muted">(movie, episode, track)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{title}</strong></td>
|
||||
|
@ -1287,7 +1414,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</tr>
|
||||
<tr>
|
||||
<td><strong>{season_num}</strong></td>
|
||||
<td>The season number for the item if item is episode.</td>
|
||||
<td>The season number for the episode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{season_num00}</strong></td>
|
||||
|
@ -1295,7 +1422,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</tr>
|
||||
<tr>
|
||||
<td><strong>{episode_num}</strong></td>
|
||||
<td>The episode number for the item if item is episode.</td>
|
||||
<td>The episode number for the episode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{episode_num00}</strong></td>
|
||||
|
@ -1303,7 +1430,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</tr>
|
||||
<tr>
|
||||
<td><strong>{track_num}</strong></td>
|
||||
<td>The track number for the item if item is track.</td>
|
||||
<td>The track number for the track.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{track_num00}</strong></td>
|
||||
|
@ -1319,7 +1446,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
</tr>
|
||||
<tr>
|
||||
<td><strong>{content_rating}</strong></td>
|
||||
<td>The content rating for the item. (e.g. TV-MA, TV-PG, etc.)</td>
|
||||
<td>The content rating for the item. <span class="small-muted">(e.g. TV-MA, TV-PG, etc.)</span></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{directors}</strong></td>
|
||||
|
@ -1353,21 +1480,65 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<td><strong>{duration}</strong></td>
|
||||
<td>The duration (in minutes) for the item.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{poster_url}</strong></td>
|
||||
<td>A URL for the movie or TV show poster.
|
||||
<p class="small-muted">(PMS agent must be Freebase or TheTVDB)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{imdb_id}</strong></td>
|
||||
<td>The IMDB ID for the movie. <span class="small-muted">(e.g. tt2488496)</span>
|
||||
<p class="small-muted">(PMS agent must be Freebase)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{imdb_url}</strong></td>
|
||||
<td>The IMDB URL for the movie.
|
||||
<p class="small-muted">(PMS agent must be Freebase)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{thetvdb_id}</strong></td>
|
||||
<td>The TVDB ID for the TV show. <span class="small-muted">(e.g. 121361)</span>
|
||||
<p class="small-muted">(PMS agent must be TheTVDB)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{thetvdb_url}</strong></td>
|
||||
<td>The TVDB URL for the TV show.
|
||||
<p class="small-muted">(PMS agent must be TheTVDB)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{themoviedb_id}</strong></td>
|
||||
<td>The TMDb ID for the movie or TV show. <span class="small-muted">(e.g. 15260)</span>
|
||||
<p class="small-muted">(PMS agent must be The Movie Database)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{themoviedb_url}</strong></td>
|
||||
<td>The TMDb URL for the movie or TV show.
|
||||
<p class="small-muted">(PMS agent must be The Movie Database)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{lastfm_url}</strong></td>
|
||||
<td>The last.fm URL for the album.
|
||||
<p class="small-muted">(PMS agent must be Last.fm)</p></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{trakt_url}</strong></td>
|
||||
<td>The trakt.tv URL for the movie or TV show.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{section_id}</strong></td>
|
||||
<td>The unique identifier for the library.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{rating_key}</strong></td>
|
||||
<td>The unique identifier for the item.</td>
|
||||
<td>The unique identifier for the movie, episode, or track.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{parent_rating_key}</strong></td>
|
||||
<td>The unique identifier for the item's parent (season or album).</td>
|
||||
<td>The unique identifier for the season or album.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>{grandparent_rating_key}</strong></td>
|
||||
<td>The unique identifier for the item's grandparent (TV show or artist).</td>
|
||||
<td>The unique identifier for the TV show or artist.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -1408,7 +1579,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<div>
|
||||
<p class="help-block">All text inside a <strong>music</strong> tag will only be sent when the media item being played back is a music track.</p>
|
||||
<p><strong style="color: #fff;">Example:</strong></p>
|
||||
<pre>{user} has started playing {title} <music>(Track {episode_num})</music></pre>
|
||||
<pre>{user} has started playing {title} <music>(Track {track_num})</music></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1454,6 +1625,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
|
|||
<%def name="javascriptIncludes()">
|
||||
<script src="interfaces/default/js/parsley.min.js"></script>
|
||||
<script src="interfaces/default/js/Sortable.min.js"></script>
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
|
||||
|
@ -1485,7 +1657,7 @@ $(document).ready(function() {
|
|||
var configForm = $("#configUpdate");
|
||||
function saveSettings() {
|
||||
if (configForm.parsley().validate()) {
|
||||
doAjaxCall('configUpdate', $(this), 'tabs', true);
|
||||
doAjaxCall('configUpdate', $(this), 'tabs', true, getSchedulerTable);
|
||||
postSaveChecks();
|
||||
return false;
|
||||
} else {
|
||||
|
@ -1564,6 +1736,20 @@ $(document).ready(function() {
|
|||
}
|
||||
});
|
||||
|
||||
if ($("#https_create_cert").is(":checked")) {
|
||||
$("#https_options_self-signed").show();
|
||||
} else {
|
||||
$("#https_options_self-signed").hide();
|
||||
}
|
||||
|
||||
$("#https_create_cert").click(function(){
|
||||
if ($("#https_create_cert").is(":checked")) {
|
||||
$("#https_options_self-signed").slideDown();
|
||||
} else {
|
||||
$("#https_options_self-signed").slideUp();
|
||||
}
|
||||
});
|
||||
|
||||
$( ".http-settings" ).change(function() {
|
||||
httpChanged = true;
|
||||
});
|
||||
|
@ -1678,10 +1864,10 @@ $(document).ready(function() {
|
|||
|
||||
// Load notification agent config modal
|
||||
$(".toggle-notification-config-modal").click(function() {
|
||||
var configId = $(this).data('id');
|
||||
var agent_id = $(this).data('id');
|
||||
$.ajax({
|
||||
url: 'get_notification_agent_config',
|
||||
data: { config_id: configId },
|
||||
data: { agent_id: agent_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
|
@ -1692,10 +1878,10 @@ $(document).ready(function() {
|
|||
|
||||
// Load notification triggers config modal
|
||||
$(".toggle-notification-triggers-modal").click(function() {
|
||||
var configId = $(this).data('id');
|
||||
var agent_id = $(this).data('id');
|
||||
$.ajax({
|
||||
url: 'get_notification_agent_triggers',
|
||||
data: { config_id: configId },
|
||||
data: { agent_id: agent_id },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
|
@ -1710,33 +1896,60 @@ $(document).ready(function() {
|
|||
})
|
||||
|
||||
$.ajax({
|
||||
url: 'get_server_pref',
|
||||
data: { pref: 'logDebug' },
|
||||
url: 'get_server_identity',
|
||||
async: true,
|
||||
success: function(data) {
|
||||
if (data !== 'true') {
|
||||
$("#debugLogCheck").html("Debug logging must be enabled on your Plex Server. <a target='_blank' href='https://support.plex.tv/hc/en-us/articles/201643703-Reporting-issues-with-Plex-Media-Server'> More..</a>");
|
||||
var version = data.version.split('.')
|
||||
if (parseInt(version[0]) >= 0 && parseInt(version[1]) >= 9 && parseInt(version[2]) >= 14) {
|
||||
$("#debugLogCheck").html("IP address is automatically logged for PMS version 0.9.14 and above.");
|
||||
$("#ip_logging_enable").attr("disabled", true);
|
||||
$("#ip_logging_enable").attr("checked", true);
|
||||
} else {
|
||||
$.ajax({
|
||||
url: 'get_server_pref',
|
||||
data: { pref: 'logDebug' },
|
||||
async: true,
|
||||
success: function(data) {
|
||||
if (data !== 'true') {
|
||||
$("#debugLogCheck").html("Debug logging must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/201643703-Reporting-issues-with-Plex-Media-Server')}'>Click here</a> for help.");
|
||||
$("#ip_logging_enable").attr("disabled", true);
|
||||
$("#ip_logging_enable").attr("checked", false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check to see if our logs folder is set before allowing IP logging to be enabled.
|
||||
checkLogsPath();
|
||||
|
||||
$("#pms_logs_folder").change(function() {
|
||||
checkLogsPath();
|
||||
});
|
||||
|
||||
function checkLogsPath() {
|
||||
if ($("#pms_logs_folder").val() == '') {
|
||||
$("#debugLogCheck").html("You must first define your Plex Server Logs folder path under the Plex Media Server tab.");
|
||||
$("#ip_logging_enable").attr("disabled", true);
|
||||
$("#ip_logging_enable").attr("checked", false);
|
||||
} else {
|
||||
$("#ip_logging_enable").attr("disabled", false);
|
||||
$("#debugLogCheck").html("");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Check to see if our logs folder is set before allowing IP logging to be enabled.
|
||||
checkLogsPath();
|
||||
|
||||
$("#pms_logs_folder").change(function() {
|
||||
checkLogsPath();
|
||||
});
|
||||
|
||||
function checkLogsPath() {
|
||||
if ($("#pms_logs_folder").val() == '') {
|
||||
$("#debugLogCheck").html("You must first define your Plex Server Logs folder path under the Plex Media Server tab.");
|
||||
$("#ip_logging_enable").attr("disabled", true);
|
||||
} else {
|
||||
$("#ip_logging_enable").attr("disabled", false);
|
||||
$("#debugLogCheck").html("");
|
||||
$.ajax({
|
||||
url: 'get_server_pref',
|
||||
data: { pref: 'PublishServerOnPlexOnlineKey' },
|
||||
async: true,
|
||||
success: function(data) {
|
||||
if (data !== 'true') {
|
||||
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
|
||||
$("#monitor_remote_access").attr("disabled", true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var accordion_session = new Accordion($('#accordion-session'), false);
|
||||
var accordion_timeline = new Accordion($('#accordion-timeline'), false);
|
||||
|
@ -1824,6 +2037,19 @@ $(document).ready(function() {
|
|||
};
|
||||
$(this).on('focus keyup input', function() { resizeTextarea(this); }).removeAttr('data-autoresize');
|
||||
});
|
||||
|
||||
function getSchedulerTable() {
|
||||
$.ajax({
|
||||
url: 'get_scheduler_table',
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function(xhr, status) {
|
||||
$("#plexpy-scheduler-table").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
getSchedulerTable();
|
||||
|
||||
});
|
||||
</script>
|
||||
</%def>
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
<%def name="headIncludes()">
|
||||
<link rel="stylesheet" href="interfaces/default/css/dataTables.bootstrap.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/dataTables.colVis.css">
|
||||
<link rel="stylesheet" href="interfaces/default/css/plexpy-dataTables.css">
|
||||
</%def>
|
||||
|
||||
|
@ -12,6 +13,7 @@
|
|||
<span><i class="fa fa-group"></i> All Users</span>
|
||||
</div>
|
||||
<div class="button-bar">
|
||||
<div class="colvis-button-bar hidden-xs"></div>
|
||||
<button class="btn btn-dark refresh-users-button" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
|
||||
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
|
||||
<i class="fa fa-pencil"></i> Edit mode
|
||||
|
@ -32,6 +34,7 @@
|
|||
<th align="left" id="last_player">Last Player</th>
|
||||
<th align="left" id="last_played">Last Played</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
<th align="left" id="total_duration">Total Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -67,6 +70,7 @@
|
|||
|
||||
<%def name="javascriptIncludes()">
|
||||
<script src="interfaces/default/js/jquery.dataTables.min.js"></script>
|
||||
<script src="interfaces/default/js/dataTables.colVis.js"></script>
|
||||
<script src="interfaces/default/js/dataTables.bootstrap.min.js"></script>
|
||||
<script src="interfaces/default/js/dataTables.bootstrap.pagination.js"></script>
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
|
@ -84,6 +88,8 @@
|
|||
}
|
||||
|
||||
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(users_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('users_list_table', users_list_table);
|
||||
|
||||
|
|
|
@ -83,7 +83,7 @@ from plexpy import common
|
|||
<div class="col-xs-4">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Force SSL
|
||||
<input type="checkbox" id="pms_ssl" name="pms_ssl" value="1"> Use SSL
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -244,7 +244,7 @@ from plexpy import common
|
|||
},
|
||||
render: {
|
||||
option: function (item, escape) {
|
||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
|
||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
||||
},
|
||||
item: function (item, escape) {
|
||||
// first item is rendered before initialization bug?
|
||||
|
@ -254,7 +254,7 @@ from plexpy import common
|
|||
.filter('[value="' + item.value + '"]').data());
|
||||
|
||||
}
|
||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '">' + item.value + '</div>';
|
||||
return '<div data-use_ssl="' + item.httpsRequired + '" data-local="' + item.local + '" data-ci="' + item.clientIdentifier + '" data-ip="' + item.ip + '" data-port="' + item.port + '" data-label="' + item.label + '">' + item.value + ' (' + item.label + ')</div>';
|
||||
}
|
||||
},
|
||||
onChange: function (item) {
|
||||
|
@ -378,8 +378,8 @@ from plexpy import common
|
|||
var pms_ip = $("#pms_ip").val().trim();
|
||||
var pms_port = $("#pms_port").val().trim();
|
||||
var pms_identifier = $("#pms_identifier").val();
|
||||
var pms_ssl = $("#pms_ssl").val();
|
||||
var pms_is_remote = $("#pms_is_remote").val();
|
||||
var pms_ssl = $("#pms_ssl").is(':checked') ? 1 : 0;
|
||||
var pms_is_remote = $("#pms_is_remote").is(':checked') ? 1 : 0;
|
||||
if ((pms_ip !== '') || (pms_port !== '')) {
|
||||
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i> Validating server...');
|
||||
$('#pms-verify-status').fadeIn('fast');
|
||||
|
|
1652
lib/IPy.py
Normal file
1652
lib/IPy.py
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,26 +1,21 @@
|
|||
# -*- coding: latin-1 -*-
|
||||
#
|
||||
# Copyright (C) Martin Sjögren and AB Strakt 2001, All rights reserved
|
||||
# Copyright (C) Jean-Paul Calderone 2008, All rights reserved
|
||||
# This file is licenced under the GNU LESSER GENERAL PUBLIC LICENSE Version 2.1 or later (aka LGPL v2.1)
|
||||
# Please see LGPL2.1.txt for more information
|
||||
# Copyright (C) AB Strakt
|
||||
# Copyright (C) Jean-Paul Calderone
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Certificate generation module.
|
||||
"""
|
||||
|
||||
from OpenSSL import crypto
|
||||
import time
|
||||
|
||||
TYPE_RSA = crypto.TYPE_RSA
|
||||
TYPE_DSA = crypto.TYPE_DSA
|
||||
|
||||
serial = int(time.time())
|
||||
|
||||
|
||||
def createKeyPair(type, bits):
|
||||
"""
|
||||
Create a public/private key pair.
|
||||
|
||||
Arguments: type - Key type, must be one of TYPE_RSA and TYPE_DSA
|
||||
bits - Number of bits to use in the key
|
||||
Returns: The public/private key pair in a PKey object
|
||||
|
@ -29,12 +24,11 @@ def createKeyPair(type, bits):
|
|||
pkey.generate_key(type, bits)
|
||||
return pkey
|
||||
|
||||
def createCertRequest(pkey, digest="md5", **name):
|
||||
def createCertRequest(pkey, digest="sha256", **name):
|
||||
"""
|
||||
Create a certificate request.
|
||||
|
||||
Arguments: pkey - The key to associate with the request
|
||||
digest - Digestion method to use for signing, default is md5
|
||||
digest - Digestion method to use for signing, default is sha256
|
||||
**name - The name of the subject of the request, possible
|
||||
arguments are:
|
||||
C - Country name
|
||||
|
@ -49,18 +43,17 @@ def createCertRequest(pkey, digest="md5", **name):
|
|||
req = crypto.X509Req()
|
||||
subj = req.get_subject()
|
||||
|
||||
for (key,value) in name.items():
|
||||
for key, value in name.items():
|
||||
setattr(subj, key, value)
|
||||
|
||||
req.set_pubkey(pkey)
|
||||
req.sign(pkey, digest)
|
||||
return req
|
||||
|
||||
def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter), digest="md5"):
|
||||
def createCertificate(req, issuerCertKey, serial, validityPeriod, digest="sha256"):
|
||||
"""
|
||||
Generate a certificate given a certificate request.
|
||||
|
||||
Arguments: req - Certificate reqeust to use
|
||||
Arguments: req - Certificate request to use
|
||||
issuerCert - The certificate of the issuer
|
||||
issuerKey - The private key of the issuer
|
||||
serial - Serial number for the certificate
|
||||
|
@ -68,9 +61,11 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
|
|||
starts being valid
|
||||
notAfter - Timestamp (relative to now) when the certificate
|
||||
stops being valid
|
||||
digest - Digest method to use for signing, default is md5
|
||||
digest - Digest method to use for signing, default is sha256
|
||||
Returns: The signed certificate in an X509 object
|
||||
"""
|
||||
issuerCert, issuerKey = issuerCertKey
|
||||
notBefore, notAfter = validityPeriod
|
||||
cert = crypto.X509()
|
||||
cert.set_serial_number(serial)
|
||||
cert.gmtime_adj_notBefore(notBefore)
|
||||
|
@ -80,3 +75,32 @@ def createCertificate(req, (issuerCert, issuerKey), serial, (notBefore, notAfter
|
|||
cert.set_pubkey(req.get_pubkey())
|
||||
cert.sign(issuerKey, digest)
|
||||
return cert
|
||||
|
||||
def createSelfSignedCertificate((issuerName, issuerKey), serial, (notBefore, notAfter), altNames, digest="sha256"):
|
||||
"""
|
||||
Generate a certificate given a certificate request.
|
||||
Arguments: issuerName - The name of the issuer
|
||||
issuerKey - The private key of the issuer
|
||||
serial - Serial number for the certificate
|
||||
notBefore - Timestamp (relative to now) when the certificate
|
||||
starts being valid
|
||||
notAfter - Timestamp (relative to now) when the certificate
|
||||
stops being valid
|
||||
altNames - The alternative names
|
||||
digest - Digest method to use for signing, default is sha256
|
||||
Returns: The signed certificate in an X509 object
|
||||
"""
|
||||
cert = crypto.X509()
|
||||
cert.set_version(2)
|
||||
cert.set_serial_number(serial)
|
||||
cert.get_subject().CN = issuerName
|
||||
cert.gmtime_adj_notBefore(notBefore)
|
||||
cert.gmtime_adj_notAfter(notAfter)
|
||||
cert.set_issuer(cert.get_subject())
|
||||
cert.set_pubkey(issuerKey)
|
||||
|
||||
if altNames:
|
||||
cert.add_extensions([crypto.X509Extension("subjectAltName", False, altNames)])
|
||||
|
||||
cert.sign(issuerKey, digest)
|
||||
return cert
|
||||
|
|
732
lib/profilehooks.py
Normal file
732
lib/profilehooks.py
Normal file
|
@ -0,0 +1,732 @@
|
|||
"""
|
||||
Profiling hooks
|
||||
|
||||
This module contains a couple of decorators (`profile` and `coverage`) that
|
||||
can be used to wrap functions and/or methods to produce profiles and line
|
||||
coverage reports. There's a third convenient decorator (`timecall`) that
|
||||
measures the duration of function execution without the extra profiling
|
||||
overhead.
|
||||
|
||||
Usage example (Python 2.4 or newer)::
|
||||
|
||||
from profilehooks import profile, coverage
|
||||
|
||||
@profile # or @coverage
|
||||
def fn(n):
|
||||
if n < 2: return 1
|
||||
else: return n * fn(n-1)
|
||||
|
||||
print fn(42)
|
||||
|
||||
Usage example (Python 2.3 or older)::
|
||||
|
||||
from profilehooks import profile, coverage
|
||||
|
||||
def fn(n):
|
||||
if n < 2: return 1
|
||||
else: return n * fn(n-1)
|
||||
|
||||
# Now wrap that function in a decorator
|
||||
fn = profile(fn) # or coverage(fn)
|
||||
|
||||
print fn(42)
|
||||
|
||||
Reports for all thusly decorated functions will be printed to sys.stdout
|
||||
on program termination. You can alternatively request for immediate
|
||||
reports for each call by passing immediate=True to the profile decorator.
|
||||
|
||||
There's also a @timecall decorator for printing the time to sys.stderr
|
||||
every time a function is called, when you just want to get a rough measure
|
||||
instead of a detailed (but costly) profile.
|
||||
|
||||
Caveats
|
||||
|
||||
A thread on python-dev convinced me that hotshot produces bogus numbers.
|
||||
See http://mail.python.org/pipermail/python-dev/2005-November/058264.html
|
||||
|
||||
I don't know what will happen if a decorated function will try to call
|
||||
another decorated function. All decorators probably need to explicitly
|
||||
support nested profiling (currently TraceFuncCoverage is the only one
|
||||
that supports this, while HotShotFuncProfile has support for recursive
|
||||
functions.)
|
||||
|
||||
Profiling with hotshot creates temporary files (*.prof for profiling,
|
||||
*.cprof for coverage) in the current directory. These files are not
|
||||
cleaned up. Exception: when you specify a filename to the profile
|
||||
decorator (to store the pstats.Stats object for later inspection),
|
||||
the temporary file will be the filename you specified with '.raw'
|
||||
appended at the end.
|
||||
|
||||
Coverage analysis with hotshot seems to miss some executions resulting
|
||||
in lower line counts and some lines errorneously marked as never
|
||||
executed. For this reason coverage analysis now uses trace.py which is
|
||||
slower, but more accurate.
|
||||
|
||||
Copyright (c) 2004--2008 Marius Gedminas <marius@pov.lt>
|
||||
Copyright (c) 2007 Hanno Schlichting
|
||||
Copyright (c) 2008 Florian Schulze
|
||||
|
||||
Released under the MIT licence since December 2006:
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
|
||||
(Previously it was distributed under the GNU General Public Licence.)
|
||||
"""
|
||||
# $Id: profilehooks.py 29 2010-08-13 16:29:20Z mg $
|
||||
|
||||
__author__ = "Marius Gedminas (marius@gedmin.as)"
|
||||
__copyright__ = "Copyright 2004-2009 Marius Gedminas"
|
||||
__license__ = "MIT"
|
||||
__version__ = "1.4"
|
||||
__date__ = "2009-03-31"
|
||||
|
||||
|
||||
import atexit
|
||||
import inspect
|
||||
import sys
|
||||
import re
|
||||
|
||||
# For profiling
|
||||
from profile import Profile
|
||||
import pstats
|
||||
|
||||
# For hotshot profiling (inaccurate!)
|
||||
try:
|
||||
import hotshot
|
||||
import hotshot.stats
|
||||
except ImportError:
|
||||
hotshot = None
|
||||
|
||||
# For trace.py coverage
|
||||
import trace
|
||||
|
||||
# For hotshot coverage (inaccurate!; uses undocumented APIs; might break)
|
||||
if hotshot is not None:
|
||||
import _hotshot
|
||||
import hotshot.log
|
||||
|
||||
# For cProfile profiling (best)
|
||||
try:
|
||||
import cProfile
|
||||
except ImportError:
|
||||
cProfile = None
|
||||
|
||||
# For timecall
|
||||
import time
|
||||
|
||||
|
||||
# registry of available profilers
|
||||
AVAILABLE_PROFILERS = {}
|
||||
|
||||
|
||||
def profile(fn=None, skip=0, filename=None, immediate=False, dirs=False,
|
||||
sort=None, entries=40,
|
||||
profiler=('cProfile', 'profile', 'hotshot')):
|
||||
"""Mark `fn` for profiling.
|
||||
|
||||
If `skip` is > 0, first `skip` calls to `fn` will not be profiled.
|
||||
|
||||
If `immediate` is False, profiling results will be printed to
|
||||
sys.stdout on program termination. Otherwise results will be printed
|
||||
after each call.
|
||||
|
||||
If `dirs` is False only the name of the file will be printed.
|
||||
Otherwise the full path is used.
|
||||
|
||||
`sort` can be a list of sort keys (defaulting to ['cumulative',
|
||||
'time', 'calls']). The following ones are recognized::
|
||||
|
||||
'calls' -- call count
|
||||
'cumulative' -- cumulative time
|
||||
'file' -- file name
|
||||
'line' -- line number
|
||||
'module' -- file name
|
||||
'name' -- function name
|
||||
'nfl' -- name/file/line
|
||||
'pcalls' -- call count
|
||||
'stdname' -- standard name
|
||||
'time' -- internal time
|
||||
|
||||
`entries` limits the output to the first N entries.
|
||||
|
||||
`profiler` can be used to select the preferred profiler, or specify a
|
||||
sequence of them, in order of preference. The default is ('cProfile'.
|
||||
'profile', 'hotshot').
|
||||
|
||||
If `filename` is specified, the profile stats will be stored in the
|
||||
named file. You can load them pstats.Stats(filename).
|
||||
|
||||
Usage::
|
||||
|
||||
def fn(...):
|
||||
...
|
||||
fn = profile(fn, skip=1)
|
||||
|
||||
If you are using Python 2.4, you should be able to use the decorator
|
||||
syntax::
|
||||
|
||||
@profile(skip=3)
|
||||
def fn(...):
|
||||
...
|
||||
|
||||
or just ::
|
||||
|
||||
@profile
|
||||
def fn(...):
|
||||
...
|
||||
|
||||
"""
|
||||
if fn is None: # @profile() syntax -- we are a decorator maker
|
||||
def decorator(fn):
|
||||
return profile(fn, skip=skip, filename=filename,
|
||||
immediate=immediate, dirs=dirs,
|
||||
sort=sort, entries=entries,
|
||||
profiler=profiler)
|
||||
return decorator
|
||||
# @profile syntax -- we are a decorator.
|
||||
if isinstance(profiler, str):
|
||||
profiler = [profiler]
|
||||
for p in profiler:
|
||||
if p in AVAILABLE_PROFILERS:
|
||||
profiler_class = AVAILABLE_PROFILERS[p]
|
||||
break
|
||||
else:
|
||||
raise ValueError('only these profilers are available: %s'
|
||||
% ', '.join(AVAILABLE_PROFILERS))
|
||||
fp = profiler_class(fn, skip=skip, filename=filename,
|
||||
immediate=immediate, dirs=dirs,
|
||||
sort=sort, entries=entries)
|
||||
# fp = HotShotFuncProfile(fn, skip=skip, filename=filename, ...)
|
||||
# or HotShotFuncProfile
|
||||
# We cannot return fp or fp.__call__ directly as that would break method
|
||||
# definitions, instead we need to return a plain function.
|
||||
def new_fn(*args, **kw):
|
||||
return fp(*args, **kw)
|
||||
new_fn.__doc__ = fn.__doc__
|
||||
new_fn.__name__ = fn.__name__
|
||||
new_fn.__dict__ = fn.__dict__
|
||||
new_fn.__module__ = fn.__module__
|
||||
return new_fn
|
||||
|
||||
|
||||
def coverage(fn):
|
||||
"""Mark `fn` for line coverage analysis.
|
||||
|
||||
Results will be printed to sys.stdout on program termination.
|
||||
|
||||
Usage::
|
||||
|
||||
def fn(...):
|
||||
...
|
||||
fn = coverage(fn)
|
||||
|
||||
If you are using Python 2.4, you should be able to use the decorator
|
||||
syntax::
|
||||
|
||||
@coverage
|
||||
def fn(...):
|
||||
...
|
||||
|
||||
"""
|
||||
fp = TraceFuncCoverage(fn) # or HotShotFuncCoverage
|
||||
# We cannot return fp or fp.__call__ directly as that would break method
|
||||
# definitions, instead we need to return a plain function.
|
||||
def new_fn(*args, **kw):
|
||||
return fp(*args, **kw)
|
||||
new_fn.__doc__ = fn.__doc__
|
||||
new_fn.__name__ = fn.__name__
|
||||
new_fn.__dict__ = fn.__dict__
|
||||
new_fn.__module__ = fn.__module__
|
||||
return new_fn
|
||||
|
||||
|
||||
def coverage_with_hotshot(fn):
|
||||
"""Mark `fn` for line coverage analysis.
|
||||
|
||||
Uses the 'hotshot' module for fast coverage analysis.
|
||||
|
||||
BUG: Produces inaccurate results.
|
||||
|
||||
See the docstring of `coverage` for usage examples.
|
||||
"""
|
||||
fp = HotShotFuncCoverage(fn)
|
||||
# We cannot return fp or fp.__call__ directly as that would break method
|
||||
# definitions, instead we need to return a plain function.
|
||||
def new_fn(*args, **kw):
|
||||
return fp(*args, **kw)
|
||||
new_fn.__doc__ = fn.__doc__
|
||||
new_fn.__name__ = fn.__name__
|
||||
new_fn.__dict__ = fn.__dict__
|
||||
new_fn.__module__ = fn.__module__
|
||||
return new_fn
|
||||
|
||||
|
||||
class FuncProfile(object):
|
||||
"""Profiler for a function (uses profile)."""
|
||||
|
||||
# This flag is shared between all instances
|
||||
in_profiler = False
|
||||
|
||||
Profile = Profile
|
||||
|
||||
def __init__(self, fn, skip=0, filename=None, immediate=False, dirs=False,
|
||||
sort=None, entries=40):
|
||||
"""Creates a profiler for a function.
|
||||
|
||||
Every profiler has its own log file (the name of which is derived
|
||||
from the function name).
|
||||
|
||||
FuncProfile registers an atexit handler that prints profiling
|
||||
information to sys.stderr when the program terminates.
|
||||
"""
|
||||
self.fn = fn
|
||||
self.skip = skip
|
||||
self.filename = filename
|
||||
self.immediate = immediate
|
||||
self.dirs = dirs
|
||||
self.sort = sort or ('cumulative', 'time', 'calls')
|
||||
if isinstance(self.sort, str):
|
||||
self.sort = (self.sort, )
|
||||
self.entries = entries
|
||||
self.reset_stats()
|
||||
atexit.register(self.atexit)
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""Profile a singe call to the function."""
|
||||
self.ncalls += 1
|
||||
if self.skip > 0:
|
||||
self.skip -= 1
|
||||
self.skipped += 1
|
||||
return self.fn(*args, **kw)
|
||||
if FuncProfile.in_profiler:
|
||||
# handle recursive calls
|
||||
return self.fn(*args, **kw)
|
||||
# You cannot reuse the same profiler for many calls and accumulate
|
||||
# stats that way. :-/
|
||||
profiler = self.Profile()
|
||||
try:
|
||||
FuncProfile.in_profiler = True
|
||||
return profiler.runcall(self.fn, *args, **kw)
|
||||
finally:
|
||||
FuncProfile.in_profiler = False
|
||||
self.stats.add(profiler)
|
||||
if self.immediate:
|
||||
self.print_stats()
|
||||
self.reset_stats()
|
||||
|
||||
def print_stats(self):
|
||||
"""Print profile information to sys.stdout."""
|
||||
funcname = self.fn.__name__
|
||||
filename = self.fn.func_code.co_filename
|
||||
lineno = self.fn.func_code.co_firstlineno
|
||||
print
|
||||
print "*** PROFILER RESULTS ***"
|
||||
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||
print "function called %d times" % self.ncalls,
|
||||
if self.skipped:
|
||||
print "(%d calls not profiled)" % self.skipped
|
||||
else:
|
||||
print
|
||||
print
|
||||
stats = self.stats
|
||||
if self.filename:
|
||||
stats.dump_stats(self.filename)
|
||||
if not self.dirs:
|
||||
stats.strip_dirs()
|
||||
stats.sort_stats(*self.sort)
|
||||
stats.print_stats(self.entries)
|
||||
|
||||
def reset_stats(self):
|
||||
"""Reset accumulated profiler statistics."""
|
||||
# Note: not using self.Profile, since pstats.Stats() fails then
|
||||
self.stats = pstats.Stats(Profile())
|
||||
self.ncalls = 0
|
||||
self.skipped = 0
|
||||
|
||||
def atexit(self):
|
||||
"""Stop profiling and print profile information to sys.stdout.
|
||||
|
||||
This function is registered as an atexit hook.
|
||||
"""
|
||||
if not self.immediate:
|
||||
self.print_stats()
|
||||
|
||||
|
||||
AVAILABLE_PROFILERS['profile'] = FuncProfile
|
||||
|
||||
|
||||
if cProfile is not None:
|
||||
|
||||
class CProfileFuncProfile(FuncProfile):
|
||||
"""Profiler for a function (uses cProfile)."""
|
||||
|
||||
Profile = cProfile.Profile
|
||||
|
||||
AVAILABLE_PROFILERS['cProfile'] = CProfileFuncProfile
|
||||
|
||||
|
||||
if hotshot is not None:
|
||||
|
||||
class HotShotFuncProfile(object):
|
||||
"""Profiler for a function (uses hotshot)."""
|
||||
|
||||
# This flag is shared between all instances
|
||||
in_profiler = False
|
||||
|
||||
def __init__(self, fn, skip=0, filename=None):
|
||||
"""Creates a profiler for a function.
|
||||
|
||||
Every profiler has its own log file (the name of which is derived
|
||||
from the function name).
|
||||
|
||||
HotShotFuncProfile registers an atexit handler that prints
|
||||
profiling information to sys.stderr when the program terminates.
|
||||
|
||||
The log file is not removed and remains there to clutter the
|
||||
current working directory.
|
||||
"""
|
||||
self.fn = fn
|
||||
self.filename = filename
|
||||
if self.filename:
|
||||
self.logfilename = filename + ".raw"
|
||||
else:
|
||||
self.logfilename = fn.__name__ + ".prof"
|
||||
self.profiler = hotshot.Profile(self.logfilename)
|
||||
self.ncalls = 0
|
||||
self.skip = skip
|
||||
self.skipped = 0
|
||||
atexit.register(self.atexit)
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""Profile a singe call to the function."""
|
||||
self.ncalls += 1
|
||||
if self.skip > 0:
|
||||
self.skip -= 1
|
||||
self.skipped += 1
|
||||
return self.fn(*args, **kw)
|
||||
if HotShotFuncProfile.in_profiler:
|
||||
# handle recursive calls
|
||||
return self.fn(*args, **kw)
|
||||
try:
|
||||
HotShotFuncProfile.in_profiler = True
|
||||
return self.profiler.runcall(self.fn, *args, **kw)
|
||||
finally:
|
||||
HotShotFuncProfile.in_profiler = False
|
||||
|
||||
def atexit(self):
|
||||
"""Stop profiling and print profile information to sys.stderr.
|
||||
|
||||
This function is registered as an atexit hook.
|
||||
"""
|
||||
self.profiler.close()
|
||||
funcname = self.fn.__name__
|
||||
filename = self.fn.func_code.co_filename
|
||||
lineno = self.fn.func_code.co_firstlineno
|
||||
print
|
||||
print "*** PROFILER RESULTS ***"
|
||||
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||
print "function called %d times" % self.ncalls,
|
||||
if self.skipped:
|
||||
print "(%d calls not profiled)" % self.skipped
|
||||
else:
|
||||
print
|
||||
print
|
||||
stats = hotshot.stats.load(self.logfilename)
|
||||
# hotshot.stats.load takes ages, and the .prof file eats megabytes, but
|
||||
# a saved stats object is small and fast
|
||||
if self.filename:
|
||||
stats.dump_stats(self.filename)
|
||||
# it is best to save before strip_dirs
|
||||
stats.strip_dirs()
|
||||
stats.sort_stats('cumulative', 'time', 'calls')
|
||||
stats.print_stats(40)
|
||||
|
||||
AVAILABLE_PROFILERS['hotshot'] = HotShotFuncProfile
|
||||
|
||||
|
||||
class HotShotFuncCoverage:
|
||||
"""Coverage analysis for a function (uses _hotshot).
|
||||
|
||||
HotShot coverage is reportedly faster than trace.py, but it appears to
|
||||
have problems with exceptions; also line counts in coverage reports
|
||||
are generally lower from line counts produced by TraceFuncCoverage.
|
||||
Is this my bug, or is it a problem with _hotshot?
|
||||
"""
|
||||
|
||||
def __init__(self, fn):
|
||||
"""Creates a profiler for a function.
|
||||
|
||||
Every profiler has its own log file (the name of which is derived
|
||||
from the function name).
|
||||
|
||||
HotShotFuncCoverage registers an atexit handler that prints
|
||||
profiling information to sys.stderr when the program terminates.
|
||||
|
||||
The log file is not removed and remains there to clutter the
|
||||
current working directory.
|
||||
"""
|
||||
self.fn = fn
|
||||
self.logfilename = fn.__name__ + ".cprof"
|
||||
self.profiler = _hotshot.coverage(self.logfilename)
|
||||
self.ncalls = 0
|
||||
atexit.register(self.atexit)
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""Profile a singe call to the function."""
|
||||
self.ncalls += 1
|
||||
return self.profiler.runcall(self.fn, args, kw)
|
||||
|
||||
def atexit(self):
|
||||
"""Stop profiling and print profile information to sys.stderr.
|
||||
|
||||
This function is registered as an atexit hook.
|
||||
"""
|
||||
self.profiler.close()
|
||||
funcname = self.fn.__name__
|
||||
filename = self.fn.func_code.co_filename
|
||||
lineno = self.fn.func_code.co_firstlineno
|
||||
print
|
||||
print "*** COVERAGE RESULTS ***"
|
||||
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||
print "function called %d times" % self.ncalls
|
||||
print
|
||||
fs = FuncSource(self.fn)
|
||||
reader = hotshot.log.LogReader(self.logfilename)
|
||||
for what, (filename, lineno, funcname), tdelta in reader:
|
||||
if filename != fs.filename:
|
||||
continue
|
||||
if what == hotshot.log.LINE:
|
||||
fs.mark(lineno)
|
||||
if what == hotshot.log.ENTER:
|
||||
# hotshot gives us the line number of the function definition
|
||||
# and never gives us a LINE event for the first statement in
|
||||
# a function, so if we didn't perform this mapping, the first
|
||||
# statement would be marked as never executed
|
||||
if lineno == fs.firstlineno:
|
||||
lineno = fs.firstcodelineno
|
||||
fs.mark(lineno)
|
||||
reader.close()
|
||||
print fs
|
||||
|
||||
|
||||
class TraceFuncCoverage:
|
||||
"""Coverage analysis for a function (uses trace module).
|
||||
|
||||
HotShot coverage analysis is reportedly faster, but it appears to have
|
||||
problems with exceptions.
|
||||
"""
|
||||
|
||||
# Shared between all instances so that nested calls work
|
||||
tracer = trace.Trace(count=True, trace=False,
|
||||
ignoredirs=[sys.prefix, sys.exec_prefix])
|
||||
|
||||
# This flag is also shared between all instances
|
||||
tracing = False
|
||||
|
||||
def __init__(self, fn):
|
||||
"""Creates a profiler for a function.
|
||||
|
||||
Every profiler has its own log file (the name of which is derived
|
||||
from the function name).
|
||||
|
||||
TraceFuncCoverage registers an atexit handler that prints
|
||||
profiling information to sys.stderr when the program terminates.
|
||||
|
||||
The log file is not removed and remains there to clutter the
|
||||
current working directory.
|
||||
"""
|
||||
self.fn = fn
|
||||
self.logfilename = fn.__name__ + ".cprof"
|
||||
self.ncalls = 0
|
||||
atexit.register(self.atexit)
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""Profile a singe call to the function."""
|
||||
self.ncalls += 1
|
||||
if TraceFuncCoverage.tracing:
|
||||
return self.fn(*args, **kw)
|
||||
try:
|
||||
TraceFuncCoverage.tracing = True
|
||||
return self.tracer.runfunc(self.fn, *args, **kw)
|
||||
finally:
|
||||
TraceFuncCoverage.tracing = False
|
||||
|
||||
def atexit(self):
|
||||
"""Stop profiling and print profile information to sys.stderr.
|
||||
|
||||
This function is registered as an atexit hook.
|
||||
"""
|
||||
funcname = self.fn.__name__
|
||||
filename = self.fn.func_code.co_filename
|
||||
lineno = self.fn.func_code.co_firstlineno
|
||||
print
|
||||
print "*** COVERAGE RESULTS ***"
|
||||
print "%s (%s:%s)" % (funcname, filename, lineno)
|
||||
print "function called %d times" % self.ncalls
|
||||
print
|
||||
fs = FuncSource(self.fn)
|
||||
for (filename, lineno), count in self.tracer.counts.items():
|
||||
if filename != fs.filename:
|
||||
continue
|
||||
fs.mark(lineno, count)
|
||||
print fs
|
||||
never_executed = fs.count_never_executed()
|
||||
if never_executed:
|
||||
print "%d lines were not executed." % never_executed
|
||||
|
||||
|
||||
class FuncSource:
|
||||
"""Source code annotator for a function."""
|
||||
|
||||
blank_rx = re.compile(r"^\s*finally:\s*(#.*)?$")
|
||||
|
||||
def __init__(self, fn):
|
||||
self.fn = fn
|
||||
self.filename = inspect.getsourcefile(fn)
|
||||
self.source, self.firstlineno = inspect.getsourcelines(fn)
|
||||
self.sourcelines = {}
|
||||
self.firstcodelineno = self.firstlineno
|
||||
self.find_source_lines()
|
||||
|
||||
def find_source_lines(self):
|
||||
"""Mark all executable source lines in fn as executed 0 times."""
|
||||
strs = trace.find_strings(self.filename)
|
||||
lines = trace.find_lines_from_code(self.fn.func_code, strs)
|
||||
self.firstcodelineno = sys.maxint
|
||||
for lineno in lines:
|
||||
self.firstcodelineno = min(self.firstcodelineno, lineno)
|
||||
self.sourcelines.setdefault(lineno, 0)
|
||||
if self.firstcodelineno == sys.maxint:
|
||||
self.firstcodelineno = self.firstlineno
|
||||
|
||||
def mark(self, lineno, count=1):
|
||||
"""Mark a given source line as executed count times.
|
||||
|
||||
Multiple calls to mark for the same lineno add up.
|
||||
"""
|
||||
self.sourcelines[lineno] = self.sourcelines.get(lineno, 0) + count
|
||||
|
||||
def count_never_executed(self):
|
||||
"""Count statements that were never executed."""
|
||||
lineno = self.firstlineno
|
||||
counter = 0
|
||||
for line in self.source:
|
||||
if self.sourcelines.get(lineno) == 0:
|
||||
if not self.blank_rx.match(line):
|
||||
counter += 1
|
||||
lineno += 1
|
||||
return counter
|
||||
|
||||
def __str__(self):
|
||||
"""Return annotated source code for the function."""
|
||||
lines = []
|
||||
lineno = self.firstlineno
|
||||
for line in self.source:
|
||||
counter = self.sourcelines.get(lineno)
|
||||
if counter is None:
|
||||
prefix = ' ' * 7
|
||||
elif counter == 0:
|
||||
if self.blank_rx.match(line):
|
||||
prefix = ' ' * 7
|
||||
else:
|
||||
prefix = '>' * 6 + ' '
|
||||
else:
|
||||
prefix = '%5d: ' % counter
|
||||
lines.append(prefix + line)
|
||||
lineno += 1
|
||||
return ''.join(lines)
|
||||
|
||||
|
||||
def timecall(fn=None, immediate=True, timer=time.time):
|
||||
"""Wrap `fn` and print its execution time.
|
||||
|
||||
Example::
|
||||
|
||||
@timecall
|
||||
def somefunc(x, y):
|
||||
time.sleep(x * y)
|
||||
|
||||
somefunc(2, 3)
|
||||
|
||||
will print the time taken by somefunc on every call. If you want just
|
||||
a summary at program termination, use
|
||||
|
||||
@timecall(immediate=False)
|
||||
|
||||
You can also choose a timing method other than the default ``time.time()``,
|
||||
e.g.:
|
||||
|
||||
@timecall(timer=time.clock)
|
||||
|
||||
"""
|
||||
if fn is None: # @timecall() syntax -- we are a decorator maker
|
||||
def decorator(fn):
|
||||
return timecall(fn, immediate=immediate, timer=timer)
|
||||
return decorator
|
||||
# @timecall syntax -- we are a decorator.
|
||||
fp = FuncTimer(fn, immediate=immediate, timer=timer)
|
||||
# We cannot return fp or fp.__call__ directly as that would break method
|
||||
# definitions, instead we need to return a plain function.
|
||||
def new_fn(*args, **kw):
|
||||
return fp(*args, **kw)
|
||||
new_fn.__doc__ = fn.__doc__
|
||||
new_fn.__name__ = fn.__name__
|
||||
new_fn.__dict__ = fn.__dict__
|
||||
new_fn.__module__ = fn.__module__
|
||||
return new_fn
|
||||
|
||||
|
||||
class FuncTimer(object):
|
||||
|
||||
def __init__(self, fn, immediate, timer):
|
||||
self.fn = fn
|
||||
self.ncalls = 0
|
||||
self.totaltime = 0
|
||||
self.immediate = immediate
|
||||
self.timer = timer
|
||||
if not immediate:
|
||||
atexit.register(self.atexit)
|
||||
|
||||
def __call__(self, *args, **kw):
|
||||
"""Profile a singe call to the function."""
|
||||
fn = self.fn
|
||||
timer = self.timer
|
||||
self.ncalls += 1
|
||||
try:
|
||||
start = timer()
|
||||
return fn(*args, **kw)
|
||||
finally:
|
||||
duration = timer() - start
|
||||
self.totaltime += duration
|
||||
if self.immediate:
|
||||
funcname = fn.__name__
|
||||
filename = fn.func_code.co_filename
|
||||
lineno = fn.func_code.co_firstlineno
|
||||
print >> sys.stderr, "\n %s (%s:%s):\n %.3f seconds\n" % (
|
||||
funcname, filename, lineno, duration)
|
||||
def atexit(self):
|
||||
if not self.ncalls:
|
||||
return
|
||||
funcname = self.fn.__name__
|
||||
filename = self.fn.func_code.co_filename
|
||||
lineno = self.fn.func_code.co_firstlineno
|
||||
print ("\n %s (%s:%s):\n"
|
||||
" %d calls, %.3f seconds (%.3f seconds per call)\n" % (
|
||||
funcname, filename, lineno, self.ncalls,
|
||||
self.totaltime, self.totaltime / self.ncalls))
|
|
@ -59,6 +59,7 @@ started = False
|
|||
DATA_DIR = None
|
||||
|
||||
CONFIG = None
|
||||
CONFIG_FILE = None
|
||||
|
||||
DB_FILE = None
|
||||
|
||||
|
@ -73,17 +74,19 @@ UMASK = None
|
|||
|
||||
POLLING_FAILOVER = False
|
||||
|
||||
|
||||
def initialize(config_file):
|
||||
with INIT_LOCK:
|
||||
|
||||
global CONFIG
|
||||
global CONFIG_FILE
|
||||
global _INITIALIZED
|
||||
global CURRENT_VERSION
|
||||
global LATEST_VERSION
|
||||
global UMASK
|
||||
global POLLING_FAILOVER
|
||||
|
||||
CONFIG = plexpy.config.Config(config_file)
|
||||
CONFIG_FILE = config_file
|
||||
|
||||
assert CONFIG is not None
|
||||
|
||||
|
@ -117,6 +120,15 @@ def initialize(config_file):
|
|||
logger.initLogger(console=not QUIET, log_dir=CONFIG.LOG_DIR,
|
||||
verbose=VERBOSE)
|
||||
|
||||
if not CONFIG.BACKUP_DIR.startswith(os.path.abspath(DATA_DIR)):
|
||||
# Put the backup dir in the data dir for now
|
||||
CONFIG.BACKUP_DIR = os.path.join(DATA_DIR, 'backups')
|
||||
if not os.path.exists(CONFIG.BACKUP_DIR):
|
||||
try:
|
||||
os.makedirs(CONFIG.BACKUP_DIR)
|
||||
except OSError as e:
|
||||
logger.error("Could not create backup dir '%s': %s", BACKUP_DIR, e)
|
||||
|
||||
if not CONFIG.CACHE_DIR.startswith(os.path.abspath(DATA_DIR)):
|
||||
# Put the cache dir in the data dir for now
|
||||
CONFIG.CACHE_DIR = os.path.join(DATA_DIR, 'cache')
|
||||
|
@ -186,7 +198,6 @@ def initialize(config_file):
|
|||
_INITIALIZED = True
|
||||
return True
|
||||
|
||||
|
||||
def daemonize():
|
||||
if threading.activeCount() != 1:
|
||||
logger.warn(
|
||||
|
@ -283,9 +294,9 @@ def initialize_scheduler():
|
|||
seconds = 0
|
||||
|
||||
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
|
||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs',
|
||||
schedule_job(plextv.get_real_pms_url, 'Refresh Plex server URLs',
|
||||
hours=12, minutes=0, seconds=0)
|
||||
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex Server Name',
|
||||
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex server name',
|
||||
hours=12, minutes=0, seconds=0)
|
||||
|
||||
if CONFIG.NOTIFY_RECENTLY_ADDED:
|
||||
|
@ -296,10 +307,10 @@ def initialize_scheduler():
|
|||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
if CONFIG.MONITOR_REMOTE_ACCESS:
|
||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||
schedule_job(activity_pinger.check_server_response, 'Check for Plex remote access',
|
||||
hours=0, minutes=0, seconds=seconds)
|
||||
else:
|
||||
schedule_job(activity_pinger.check_server_response, 'Check for server response',
|
||||
schedule_job(activity_pinger.check_server_response, 'Check for Plex remote access',
|
||||
hours=0, minutes=0, seconds=0)
|
||||
|
||||
# If we're not using websockets then fall back to polling
|
||||
|
@ -322,6 +333,8 @@ def initialize_scheduler():
|
|||
schedule_job(pmsconnect.refresh_libraries, 'Refresh libraries list',
|
||||
hours=hours, minutes=0, seconds=0)
|
||||
|
||||
schedule_job(database.make_backup, 'Backup PlexPy database', hours=6, minutes=0, seconds=0, args=(True, True))
|
||||
|
||||
# Start scheduler
|
||||
if start_jobs and len(SCHED.get_jobs()):
|
||||
try:
|
||||
|
@ -333,7 +346,7 @@ def initialize_scheduler():
|
|||
#SCHED.print_jobs()
|
||||
|
||||
|
||||
def schedule_job(function, name, hours=0, minutes=0, seconds=0):
|
||||
def schedule_job(function, name, hours=0, minutes=0, seconds=0, args=None):
|
||||
"""
|
||||
Start scheduled job if starting or restarting plexpy.
|
||||
Reschedule job if Interval Settings have changed.
|
||||
|
@ -348,11 +361,11 @@ def schedule_job(function, name, hours=0, minutes=0, seconds=0):
|
|||
logger.info("Removed background task: %s", name)
|
||||
elif job.trigger.interval != datetime.timedelta(hours=hours, minutes=minutes):
|
||||
SCHED.reschedule_job(name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds))
|
||||
hours=hours, minutes=minutes, seconds=seconds), args=args)
|
||||
logger.info("Re-scheduled background task: %s", name)
|
||||
elif hours > 0 or minutes > 0 or seconds > 0:
|
||||
SCHED.add_job(function, id=name, trigger=IntervalTrigger(
|
||||
hours=hours, minutes=minutes, seconds=seconds))
|
||||
hours=hours, minutes=minutes, seconds=seconds), args=args)
|
||||
logger.info("Scheduled background task: %s", name)
|
||||
|
||||
|
||||
|
@ -801,6 +814,7 @@ def dbcheck():
|
|||
conn_db.commit()
|
||||
c_db.close()
|
||||
|
||||
|
||||
def shutdown(restart=False, update=False):
|
||||
cherrypy.engine.exit()
|
||||
SCHED.shutdown(wait=False)
|
||||
|
@ -833,6 +847,7 @@ def shutdown(restart=False, update=False):
|
|||
|
||||
os._exit(0)
|
||||
|
||||
|
||||
def generate_uuid():
|
||||
logger.debug(u"Generating UUID...")
|
||||
return uuid.uuid4().hex
|
||||
|
|
|
@ -182,7 +182,7 @@ class ActivityProcessor(object):
|
|||
self.db.action(query=query, args=args)
|
||||
|
||||
# Check if we should group the session, select the last two rows from the user
|
||||
query = 'SELECT id, rating_key, user_id, reference_id FROM session_history \
|
||||
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history \
|
||||
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
|
||||
|
||||
args = [session['user_id']]
|
||||
|
@ -191,6 +191,7 @@ class ActivityProcessor(object):
|
|||
|
||||
new_session = {'id': result[0]['id'],
|
||||
'rating_key': result[0]['rating_key'],
|
||||
'view_offset': result[0]['view_offset'],
|
||||
'user_id': result[0]['user_id'],
|
||||
'reference_id': result[0]['reference_id']}
|
||||
|
||||
|
@ -199,12 +200,14 @@ class ActivityProcessor(object):
|
|||
else:
|
||||
prev_session = {'id': result[1]['id'],
|
||||
'rating_key': result[1]['rating_key'],
|
||||
'view_offset': result[1]['view_offset'],
|
||||
'user_id': result[1]['user_id'],
|
||||
'reference_id': result[1]['reference_id']}
|
||||
|
||||
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
|
||||
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
|
||||
if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key']):
|
||||
if (prev_session is not None) and (prev_session['rating_key'] == new_session['rating_key'] \
|
||||
and prev_session['view_offset'] <= new_session['view_offset']):
|
||||
args = [prev_session['reference_id'], new_session['id']]
|
||||
else:
|
||||
args = [new_session['id'], new_session['id']]
|
||||
|
|
491
plexpy/api2.py
Normal file
491
plexpy/api2.py
Normal file
|
@ -0,0 +1,491 @@
|
|||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# 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/>.
|
||||
|
||||
|
||||
import hashlib
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import cherrypy
|
||||
import xmltodict
|
||||
|
||||
import database
|
||||
import logger
|
||||
import plexpy
|
||||
|
||||
|
||||
class API2:
|
||||
def __init__(self, **kwargs):
|
||||
self._api_valid_methods = self._api_docs().keys()
|
||||
self._api_authenticated = False
|
||||
self._api_out_type = 'json' # default
|
||||
self._api_msg = None
|
||||
self._api_debug = None
|
||||
self._api_cmd = None
|
||||
self._api_apikey = None
|
||||
self._api_callback = None # JSONP
|
||||
self._api_result_type = 'failed'
|
||||
self._api_profileme = None # For profiling the api call
|
||||
self._api_kwargs = None # Cleaned kwargs
|
||||
|
||||
def _api_docs(self, md=False):
|
||||
""" Makes the api docs """
|
||||
|
||||
docs = {}
|
||||
for f, _ in inspect.getmembers(self, predicate=inspect.ismethod):
|
||||
if not f.startswith('_') and not f.startswith('_api'):
|
||||
if md is True:
|
||||
docs[f] = inspect.getdoc(getattr(self, f)) if inspect.getdoc(getattr(self, f)) else None
|
||||
else:
|
||||
docs[f] = ' '.join(inspect.getdoc(getattr(self, f)).split()) if inspect.getdoc(getattr(self, f)) else None
|
||||
return docs
|
||||
|
||||
def docs_md(self):
|
||||
""" Return a API.md to simplify api docs because of the decorator. """
|
||||
|
||||
return self._api_make_md()
|
||||
|
||||
def docs(self):
|
||||
""" Returns a dict where commands are keys, docstring are value. """
|
||||
|
||||
return self._api_docs()
|
||||
|
||||
def _api_validate(self, *args, **kwargs):
|
||||
""" sets class vars and remove unneeded parameters. """
|
||||
|
||||
if not plexpy.CONFIG.API_ENABLED:
|
||||
self._api_msg = 'API not enabled'
|
||||
|
||||
elif not plexpy.CONFIG.API_KEY:
|
||||
self._api_msg = 'API key not generated'
|
||||
|
||||
elif len(plexpy.CONFIG.API_KEY) != 32:
|
||||
self._api_msg = 'API key not generated correctly'
|
||||
|
||||
elif 'apikey' not in kwargs:
|
||||
self._api_msg = 'Parameter apikey is required'
|
||||
|
||||
elif kwargs.get('apikey', '') != plexpy.CONFIG.API_KEY:
|
||||
self._api_msg = 'Invalid apikey'
|
||||
|
||||
elif 'cmd' not in kwargs:
|
||||
self._api_msg = 'Parameter cmd is required. Possible commands are: %s' % ', '.join(self._api_valid_methods)
|
||||
|
||||
elif 'cmd' in kwargs and kwargs.get('cmd') not in self._api_valid_methods:
|
||||
self._api_msg = 'Unknown command: %s. Possible commands are: %s' % (kwargs.get('cmd', ''), ', '.join(self._api_valid_methods))
|
||||
|
||||
self._api_callback = kwargs.pop('callback', None)
|
||||
self._api_apikey = kwargs.pop('apikey', None)
|
||||
self._api_cmd = kwargs.pop('cmd', None)
|
||||
self._api_debug = kwargs.pop('debug', False)
|
||||
self._api_profileme = kwargs.pop('profileme', None)
|
||||
# Allow override for the api.
|
||||
self._api_out_type = kwargs.pop('out_type', 'json')
|
||||
|
||||
if self._api_apikey == plexpy.CONFIG.API_KEY and plexpy.CONFIG.API_ENABLED and self._api_cmd in self._api_valid_methods:
|
||||
self._api_authenticated = True
|
||||
self._api_msg = None
|
||||
self._api_kwargs = kwargs
|
||||
elif self._api_cmd in ('get_apikey', 'docs', 'docs_md') and plexpy.CONFIG.API_ENABLED:
|
||||
self._api_authenticated = True
|
||||
# Remove the old error msg
|
||||
self._api_msg = None
|
||||
self._api_kwargs = kwargs
|
||||
|
||||
logger.debug(u'PlexPy APIv2 :: Cleaned kwargs %s' % self._api_kwargs)
|
||||
|
||||
return self._api_kwargs
|
||||
|
||||
def get_logs(self, sort='', search='', order='desc', regex='', start=0, end=0, **kwargs):
|
||||
"""
|
||||
Returns the log
|
||||
|
||||
Args:
|
||||
sort(string, optional): time, thread, msg, loglevel
|
||||
search(string, optional): 'string'
|
||||
order(string, optional): desc, asc
|
||||
regex(string, optional): 'regexstring'
|
||||
start(int, optional): int
|
||||
end(int, optional): int
|
||||
|
||||
|
||||
Returns:
|
||||
```{"response":
|
||||
{"msg": "Hey",
|
||||
"result": "success"},
|
||||
"data": [
|
||||
{"time": "29-sept.2015",
|
||||
"thread: "MainThread",
|
||||
"msg: "Called x from y",
|
||||
"loglevel": "DEBUG"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
logfile = os.path.join(plexpy.CONFIG.LOG_DIR, 'plexpy.log')
|
||||
templog = []
|
||||
start = int(kwargs.get('start', 0))
|
||||
end = int(kwargs.get('end', 0))
|
||||
|
||||
if regex:
|
||||
logger.debug(u'PlexPy APIv2 :: Filtering log using regex %s' % regex)
|
||||
reg = re.compile('u' + regex, flags=re.I)
|
||||
|
||||
for line in open(logfile, 'r').readlines():
|
||||
temp_loglevel_and_time = None
|
||||
|
||||
try:
|
||||
temp_loglevel_and_time = line.split('- ')
|
||||
loglvl = temp_loglevel_and_time[1].split(' :')[0].strip()
|
||||
tl_tread = line.split(' :: ')
|
||||
if loglvl is None:
|
||||
msg = line.replace('\n', '')
|
||||
else:
|
||||
msg = line.split(' : ')[1].replace('\n', '')
|
||||
thread = tl_tread[1].split(' : ')[0]
|
||||
except IndexError:
|
||||
# We assume this is a traceback
|
||||
tl = (len(templog) - 1)
|
||||
templog[tl]['msg'] += line.replace('\n', '')
|
||||
continue
|
||||
|
||||
if len(line) > 1 and temp_loglevel_and_time is not None and loglvl in line:
|
||||
|
||||
d = {
|
||||
'time': temp_loglevel_and_time[0],
|
||||
'loglevel': loglvl,
|
||||
'msg': msg.replace('\n', ''),
|
||||
'thread': thread
|
||||
}
|
||||
templog.append(d)
|
||||
|
||||
if end > 0 or start > 0:
|
||||
logger.debug(u'PlexPy APIv2 :: Slicing the log from %s to %s' % (start, end))
|
||||
templog = templog[start:end]
|
||||
|
||||
if sort:
|
||||
logger.debug(u'PlexPy APIv2 :: Sorting log based on %s' % sort)
|
||||
templog = sorted(templog, key=lambda k: k[sort])
|
||||
|
||||
if search:
|
||||
logger.debug(u'PlexPy APIv2 :: Searching log values for %s' % search)
|
||||
tt = [d for d in templog for k, v in d.items() if search.lower() in v.lower()]
|
||||
|
||||
if len(tt):
|
||||
templog = tt
|
||||
|
||||
if regex:
|
||||
tt = []
|
||||
for l in templog:
|
||||
stringdict = ' '.join('{}{}'.format(k, v) for k, v in l.items())
|
||||
if reg.search(stringdict):
|
||||
tt.append(l)
|
||||
|
||||
if len(tt):
|
||||
templog = tt
|
||||
|
||||
if order == 'desc':
|
||||
templog = templog[::-1]
|
||||
|
||||
self.data = templog
|
||||
return templog
|
||||
|
||||
def get_settings(self, key=''):
|
||||
""" Fetches all settings from the config file
|
||||
|
||||
Args:
|
||||
key(string, optional): 'Run the it without args to see all args'
|
||||
|
||||
Returns:
|
||||
json:
|
||||
```
|
||||
{General: {api_enabled: true, ...}
|
||||
Advanced: {cache_sizemb: "32", ...}}
|
||||
```
|
||||
"""
|
||||
|
||||
interface_dir = os.path.join(plexpy.PROG_DIR, 'data/interfaces/')
|
||||
interface_list = [name for name in os.listdir(interface_dir) if
|
||||
os.path.isdir(os.path.join(interface_dir, name))]
|
||||
|
||||
conf = plexpy.CONFIG._config
|
||||
config = {}
|
||||
|
||||
# Truthify the dict
|
||||
for k, v in conf.iteritems():
|
||||
if isinstance(v, dict):
|
||||
d = {}
|
||||
for kk, vv in v.iteritems():
|
||||
if vv == '0' or vv == '1':
|
||||
d[kk] = bool(vv)
|
||||
else:
|
||||
d[kk] = vv
|
||||
config[k] = d
|
||||
if k == 'General':
|
||||
config[k]['interface'] = interface_dir
|
||||
config[k]['interface_list'] = interface_list
|
||||
|
||||
if key:
|
||||
return config.get(key, None)
|
||||
|
||||
return config
|
||||
|
||||
def sql(self, query=''):
|
||||
""" Query the db with raw sql, makes backup of
|
||||
the db if the backup is older then 24h
|
||||
"""
|
||||
if not plexpy.CONFIG.API_SQL or not query:
|
||||
return
|
||||
|
||||
# allow the user to shoot them self
|
||||
# in the foot but not in the head..
|
||||
if not len(os.listdir(plexpy.BACKUP_DIR)):
|
||||
self.backupdb()
|
||||
else:
|
||||
# If the backup is less then 24 h old lets make a backup
|
||||
if any([os.path.getctime(os.path.join(plexpy.BACKUP_DIR, file_)) <
|
||||
(time.time() - 86400) for file_ in os.listdir(plexpy.BACKUP_DIR)]):
|
||||
self.backupdb()
|
||||
|
||||
db = database.MonitorDatabase()
|
||||
rows = db.select(query)
|
||||
self.data = rows
|
||||
return rows
|
||||
|
||||
def backupdb(self):
|
||||
""" Creates a manual backup of the plexpy.db file """
|
||||
|
||||
data = database.make_backup()
|
||||
|
||||
if data:
|
||||
self.result_type = 'success'
|
||||
else:
|
||||
self.result_type = 'failed'
|
||||
|
||||
return data
|
||||
|
||||
def restart(self, **kwargs):
|
||||
""" Restarts plexpy """
|
||||
|
||||
plexpy.SIGNAL = 'restart'
|
||||
self.msg = 'Restarting plexpy'
|
||||
self.result_type = 'success'
|
||||
|
||||
def update(self, **kwargs):
|
||||
""" Check for updates on Github """
|
||||
|
||||
plexpy.SIGNAL = 'update'
|
||||
self.msg = 'Updating plexpy'
|
||||
self.result_type = 'success'
|
||||
|
||||
def _api_make_md(self):
|
||||
""" Tries to make a API.md to simplify the api docs """
|
||||
|
||||
head = '''# API Reference\n
|
||||
The API is still pretty new and needs some serious cleaning up on the backend but should be reasonably functional. There are no error codes yet.
|
||||
|
||||
## General structure
|
||||
The API endpoint is `http://ip:port + HTTP_ROOT + /api?apikey=$apikey&cmd=$command`
|
||||
|
||||
Response example
|
||||
```
|
||||
{
|
||||
"response": {
|
||||
"data": [
|
||||
{
|
||||
"loglevel": "INFO",
|
||||
"msg": "Signal 2 caught, saving and exiting...",
|
||||
"thread": "MainThread",
|
||||
"time": "22-sep-2015 01:42:56 "
|
||||
}
|
||||
],
|
||||
"message": null,
|
||||
"result": "success"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
General parameters:
|
||||
out_type: 'xml',
|
||||
callback: 'pong',
|
||||
'debug': 1
|
||||
|
||||
## API methods'''
|
||||
|
||||
body = ''
|
||||
doc = self._api_docs(md=True)
|
||||
for k in sorted(doc):
|
||||
v = doc.get(k)
|
||||
body += '### %s\n' % k
|
||||
body += '' if not v else v + '\n'
|
||||
body += '\n\n'
|
||||
|
||||
result = head + '\n\n' + body
|
||||
return '<div style="white-space: pre-wrap">' + result + '</div>'
|
||||
|
||||
def get_apikey(self, username='', password=''):
|
||||
""" Fetches apikey
|
||||
|
||||
Args:
|
||||
username(string, optional): Your username
|
||||
password(string, optional): Your password
|
||||
|
||||
Returns:
|
||||
string: Apikey, args are required if auth is enabled
|
||||
makes and saves the apikey it does not exist
|
||||
"""
|
||||
|
||||
apikey = hashlib.sha224(str(random.getrandbits(256))).hexdigest()[0:32]
|
||||
if plexpy.CONFIG.HTTP_USERNAME and plexpy.CONFIG.HTTP_PASSWORD:
|
||||
if username == plexpy.HTTP_USERNAME and password == plexpy.CONFIG.HTTP_PASSWORD:
|
||||
if plexpy.CONFIG.API_KEY:
|
||||
self.data = plexpy.CONFIG.API_KEY
|
||||
else:
|
||||
self.data = apikey
|
||||
plexpy.CONFIG.API_KEY = apikey
|
||||
plexpy.CONFIG.write()
|
||||
else:
|
||||
self.msg = 'Authentication is enabled, please add the correct username and password to the parameters'
|
||||
else:
|
||||
if plexpy.CONFIG.API_KEY:
|
||||
self.data = plexpy.CONFIG.API_KEY
|
||||
else:
|
||||
# Make a apikey if the doesn't exist
|
||||
self.data = apikey
|
||||
plexpy.CONFIG.API_KEY = apikey
|
||||
plexpy.CONFIG.write()
|
||||
|
||||
return self.data
|
||||
|
||||
def _api_responds(self, result_type='success', data=None, msg=''):
|
||||
""" Formats the result to a predefined dict so we can hange it the to
|
||||
the desired output by _api_out_as """
|
||||
|
||||
if data is None:
|
||||
data = {}
|
||||
return {"response": {"result": result_type, "message": msg, "data": data}}
|
||||
|
||||
def _api_out_as(self, out):
|
||||
""" Formats the response to the desired output """
|
||||
|
||||
if self._api_cmd == 'docs_md':
|
||||
return out['response']['data']
|
||||
|
||||
if self._api_out_type == 'json':
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||
try:
|
||||
if self._api_debug:
|
||||
out = json.dumps(out, indent=4, sort_keys=True)
|
||||
else:
|
||||
out = json.dumps(out)
|
||||
if self._api_callback is not None:
|
||||
cherrypy.response.headers['Content-Type'] = 'application/javascript'
|
||||
# wrap with JSONP call if requested
|
||||
out = self._api_callback + '(' + out + ');'
|
||||
# if we fail to generate the output fake an error
|
||||
except Exception as e:
|
||||
logger.info(u'PlexPy APIv2 :: ' + traceback.format_exc())
|
||||
out['message'] = traceback.format_exc()
|
||||
out['result'] = 'error'
|
||||
elif self._api_out_type == 'xml':
|
||||
cherrypy.response.headers['Content-Type'] = 'application/xml'
|
||||
try:
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
except Exception as e:
|
||||
logger.error(u'PlexPy APIv2 :: Failed to parse xml result')
|
||||
try:
|
||||
out['message'] = e
|
||||
out['result'] = 'error'
|
||||
out = xmltodict.unparse(out, pretty=True)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(u'PlexPy APIv2 :: Failed to parse xml result error message %s' % e)
|
||||
out = '''<?xml version="1.0" encoding="utf-8"?>
|
||||
<response>
|
||||
<message>%s</message>
|
||||
<data></data>
|
||||
<result>error</result>
|
||||
</response>
|
||||
''' % e
|
||||
|
||||
return out
|
||||
|
||||
def _api_run(self, *args, **kwargs):
|
||||
""" handles the stuff from the handler """
|
||||
|
||||
result = {}
|
||||
logger.debug(u'PlexPy APIv2 :: Original kwargs was %s' % kwargs)
|
||||
|
||||
self._api_validate(**kwargs)
|
||||
|
||||
if self._api_cmd and self._api_authenticated:
|
||||
call = getattr(self, self._api_cmd)
|
||||
|
||||
# Profile is written to console.
|
||||
if self._api_profileme:
|
||||
from profilehooks import profile
|
||||
call = profile(call, immediate=True)
|
||||
|
||||
# We allow this to fail so we get a
|
||||
# traceback in the browser
|
||||
if self._api_debug:
|
||||
result = call(**self._api_kwargs)
|
||||
else:
|
||||
try:
|
||||
result = call(**self._api_kwargs)
|
||||
except Exception as e:
|
||||
logger.error(u'PlexPy APIv2 :: Failed to run %s %s %s' % (self._api_cmd, self._api_kwargs, e))
|
||||
|
||||
ret = None
|
||||
# The api decorated function can return different result types.
|
||||
# convert it to a list/dict before we change it to the users
|
||||
# wanted output
|
||||
try:
|
||||
if isinstance(result, (dict, list)):
|
||||
ret = result
|
||||
else:
|
||||
raise
|
||||
except:
|
||||
try:
|
||||
ret = json.loads(result)
|
||||
except (ValueError, TypeError):
|
||||
try:
|
||||
ret = xmltodict.parse(result, attr_prefix='')
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback if we cant "parse the reponse"
|
||||
if ret is None:
|
||||
ret = result
|
||||
|
||||
if ret or self._api_result_type == 'success':
|
||||
# To allow override for restart etc
|
||||
# if the call returns some data we are gonna assume its a success
|
||||
self._api_result_type = 'success'
|
||||
else:
|
||||
self._api_result_type = 'error'
|
||||
|
||||
return self._api_out_as(self._api_responds(result_type=self._api_result_type, msg=self._api_msg, data=ret))
|
|
@ -57,4 +57,15 @@ MEDIA_FLAGS_AUDIO = {'ac.?3': 'dolby_digital',
|
|||
MEDIA_FLAGS_VIDEO = {'avc1': 'h264',
|
||||
'wmv(1|2)': 'wmv',
|
||||
'wmv3': 'wmvhd'
|
||||
}
|
||||
}
|
||||
|
||||
SCHEDULER_LIST = ['Check GitHub for updates',
|
||||
'Check for active sessions',
|
||||
'Check for recently added items',
|
||||
'Check for Plex remote access',
|
||||
'Refresh users list',
|
||||
'Refresh libraries list',
|
||||
'Refresh Plex server URLs',
|
||||
'Refresh Plex server name',
|
||||
'Backup PlexPy database'
|
||||
]
|
|
@ -25,13 +25,15 @@ _CONFIG_DEFINITIONS = {
|
|||
'PMS_NAME': (unicode, 'PMS', ''),
|
||||
'PMS_PORT': (int, 'PMS', 32400),
|
||||
'PMS_TOKEN': (str, 'PMS', ''),
|
||||
'PMS_SSL': (int, 'General', 0),
|
||||
'PMS_SSL': (int, 'PMS', 0),
|
||||
'PMS_URL': (str, 'PMS', ''),
|
||||
'PMS_USE_BIF': (int, 'PMS', 0),
|
||||
'PMS_UUID': (str, 'PMS', ''),
|
||||
'TIME_FORMAT': (str, 'General', 'HH:mm'),
|
||||
'ANON_REDIRECT': (str, 'General', 'http://dereferer.org/?'),
|
||||
'API_ENABLED': (int, 'General', 0),
|
||||
'API_KEY': (str, 'General', ''),
|
||||
'API_SQL': (int, 'General', 0),
|
||||
'BOXCAR_ENABLED': (int, 'Boxcar', 0),
|
||||
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
|
||||
'BOXCAR_SOUND': (str, 'Boxcar', ''),
|
||||
|
@ -48,6 +50,7 @@ _CONFIG_DEFINITIONS = {
|
|||
'BOXCAR_ON_INTUP': (int, 'Boxcar', 0),
|
||||
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
|
||||
'BUFFER_WAIT': (int, 'Monitoring', 900),
|
||||
'BACKUP_DIR': (str, 'General', ''),
|
||||
'CACHE_DIR': (str, 'General', ''),
|
||||
'CACHE_SIZEMB': (int, 'Advanced', 32),
|
||||
'CHECK_GITHUB': (int, 'General', 1),
|
||||
|
@ -85,6 +88,8 @@ _CONFIG_DEFINITIONS = {
|
|||
'FACEBOOK_APP_SECRET': (str, 'Facebook', ''),
|
||||
'FACEBOOK_TOKEN': (str, 'Facebook', ''),
|
||||
'FACEBOOK_GROUP': (str, 'Facebook', ''),
|
||||
'FACEBOOK_INCL_POSTER': (int, 'Facebook', 1),
|
||||
'FACEBOOK_INCL_SUBJECT': (int, 'Facebook', 1),
|
||||
'FACEBOOK_ON_PLAY': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_STOP': (int, 'Facebook', 0),
|
||||
'FACEBOOK_ON_PAUSE': (int, 'Facebook', 0),
|
||||
|
@ -127,8 +132,11 @@ _CONFIG_DEFINITIONS = {
|
|||
'HOME_STATS_COUNT': (int, 'General', 5),
|
||||
'HOME_STATS_CARDS': (list, 'General', ['top_tv', 'popular_tv', 'top_movies', 'popular_movies', 'top_music', \
|
||||
'popular_music', 'last_watched', 'top_users', 'top_platforms', 'most_concurrent']),
|
||||
'HTTPS_CREATE_CERT': (int, 'General', 1),
|
||||
'HTTPS_CERT': (str, 'General', ''),
|
||||
'HTTPS_KEY': (str, 'General', ''),
|
||||
'HTTPS_DOMAIN': (str, 'General', 'localhost'),
|
||||
'HTTPS_IP': (str, 'General', '127.0.0.1'),
|
||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||
'HTTP_PASSWORD': (str, 'General', ''),
|
||||
'HTTP_PORT': (int, 'General', 8181),
|
||||
|
@ -304,6 +312,7 @@ _CONFIG_DEFINITIONS = {
|
|||
'SLACK_HOOK': (str, 'Slack', ''),
|
||||
'SLACK_CHANNEL': (str, 'Slack', ''),
|
||||
'SLACK_ICON_EMOJI': (str, 'Slack', ''),
|
||||
'SLACK_INCL_SUBJECT': (int, 'Slack', 1),
|
||||
'SLACK_USERNAME': (str, 'Slack', ''),
|
||||
'SLACK_ON_PLAY': (int, 'Slack', 0),
|
||||
'SLACK_ON_STOP': (int, 'Slack', 0),
|
||||
|
@ -343,6 +352,7 @@ _CONFIG_DEFINITIONS = {
|
|||
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
|
||||
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
|
||||
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
|
||||
'TELEGRAM_INCL_SUBJECT': (int, 'Telegram', 1),
|
||||
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
|
||||
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
|
||||
|
@ -364,6 +374,7 @@ _CONFIG_DEFINITIONS = {
|
|||
'TWITTER_ACCESS_TOKEN_SECRET': (str, 'Twitter', ''),
|
||||
'TWITTER_CONSUMER_KEY': (str, 'Twitter', ''),
|
||||
'TWITTER_CONSUMER_SECRET': (str, 'Twitter', ''),
|
||||
'TWITTER_INCL_SUBJECT': (int, 'Twitter', 1),
|
||||
'TWITTER_ON_PLAY': (int, 'Twitter', 0),
|
||||
'TWITTER_ON_STOP': (int, 'Twitter', 0),
|
||||
'TWITTER_ON_PAUSE': (int, 'Twitter', 0),
|
||||
|
|
|
@ -13,20 +13,24 @@
|
|||
# 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
|
||||
|
||||
import sqlite3
|
||||
import arrow
|
||||
import os
|
||||
import plexpy
|
||||
import time
|
||||
import sqlite3
|
||||
import shutil
|
||||
import threading
|
||||
|
||||
import logger
|
||||
import plexpy
|
||||
|
||||
|
||||
db_lock = threading.Lock()
|
||||
|
||||
|
||||
def drop_session_db():
|
||||
monitor_db = MonitorDatabase()
|
||||
monitor_db.action('DROP TABLE sessions')
|
||||
|
||||
|
||||
def clear_history_tables():
|
||||
logger.debug(u"PlexPy Database :: Deleting all session_history records... No turning back now bub.")
|
||||
monitor_db = MonitorDatabase()
|
||||
|
@ -35,10 +39,52 @@ def clear_history_tables():
|
|||
monitor_db.action('DELETE FROM session_history_metadata')
|
||||
monitor_db.action('VACUUM;')
|
||||
|
||||
|
||||
def db_filename(filename="plexpy.db"):
|
||||
""" Returns the filepath to the db """
|
||||
|
||||
return os.path.join(plexpy.DATA_DIR, filename)
|
||||
|
||||
|
||||
def make_backup(cleanup=False, scheduler=False):
|
||||
""" Makes a backup of db, removes all but the last 5 backups """
|
||||
|
||||
if scheduler:
|
||||
backup_file = 'plexpy.backup-%s.sched.db' % arrow.now().format('YYYYMMDDHHmmss')
|
||||
else:
|
||||
backup_file = 'plexpy.backup-%s.db' % arrow.now().format('YYYYMMDDHHmmss')
|
||||
backup_folder = plexpy.CONFIG.BACKUP_DIR
|
||||
backup_file_fp = os.path.join(backup_folder, backup_file)
|
||||
|
||||
# In case the user has deleted it manually
|
||||
if not os.path.exists(backup_folder):
|
||||
os.makedirs(backup_folder)
|
||||
|
||||
db = MonitorDatabase()
|
||||
db.connection.execute('begin immediate')
|
||||
shutil.copyfile(db_filename(), backup_file_fp)
|
||||
db.connection.rollback()
|
||||
|
||||
if cleanup:
|
||||
# Delete all scheduled backup files except from the last 5.
|
||||
for root, dirs, files in os.walk(backup_folder):
|
||||
db_files = [os.path.join(root, f) for f in files if f.endswith('.sched.db')]
|
||||
if len(db_files) > 5:
|
||||
backups_sorted_on_age = sorted(db_files, key=os.path.getctime, reverse=True)
|
||||
for file_ in backups_sorted_on_age[5:]:
|
||||
try:
|
||||
os.remove(file_)
|
||||
except OSError as e:
|
||||
logger.error(u"PlexPy Database :: Failed to delete %s from the backup folder: %s" % (file_, e))
|
||||
|
||||
if backup_file in os.listdir(backup_folder):
|
||||
logger.debug(u"PlexPy Database :: Successfully backed up %s to %s" % (db_filename(), backup_file))
|
||||
return True
|
||||
else:
|
||||
logger.warn(u"PlexPy Database :: Failed to backup %s to %s" % (db_filename(), backup_file))
|
||||
return False
|
||||
|
||||
|
||||
def get_cache_size():
|
||||
# This will protect against typecasting problems produced by empty string and None settings
|
||||
if not plexpy.CONFIG.CACHE_SIZEMB:
|
||||
|
@ -46,6 +92,7 @@ def get_cache_size():
|
|||
return 0
|
||||
return int(plexpy.CONFIG.CACHE_SIZEMB)
|
||||
|
||||
|
||||
def dict_factory(cursor, row):
|
||||
d = {}
|
||||
for idx, col in enumerate(cursor.description):
|
||||
|
@ -87,15 +134,15 @@ class MonitorDatabase(object):
|
|||
|
||||
except sqlite3.OperationalError, e:
|
||||
if "unable to open database file" in e.message or "database is locked" in e.message:
|
||||
logger.warn('Database Error: %s', e)
|
||||
logger.warn(u"PlexPy Database :: Database Error: %s", e)
|
||||
attempts += 1
|
||||
time.sleep(1)
|
||||
else:
|
||||
logger.error('Database error: %s', e)
|
||||
logger.error(u"PlexPy Database :: Database error: %s", e)
|
||||
raise
|
||||
|
||||
except sqlite3.DatabaseError, e:
|
||||
logger.error('Fatal Error executing %s :: %s', query, e)
|
||||
logger.error(u"PlexPy Database :: Fatal Error executing %s :: %s", query, e)
|
||||
raise
|
||||
|
||||
return sql_result
|
||||
|
@ -139,7 +186,7 @@ class MonitorDatabase(object):
|
|||
try:
|
||||
self.action(insert_query, value_dict.values() + key_dict.values())
|
||||
except sqlite3.IntegrityError:
|
||||
logger.info('Queries failed: %s and %s', update_query, insert_query)
|
||||
logger.info(u"PlexPy Database :: Queries failed: %s and %s", update_query, insert_query)
|
||||
|
||||
# We want to know if it was an update or insert
|
||||
return trans_type
|
||||
|
|
|
@ -58,7 +58,7 @@ class DataFactory(object):
|
|||
'session_history_metadata.thumb',
|
||||
'session_history_metadata.parent_thumb',
|
||||
'session_history_metadata.grandparent_thumb',
|
||||
'((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \
|
||||
'MAX((CASE WHEN view_offset IS NULL THEN 0.1 ELSE view_offset * 1.0 END) / \
|
||||
(CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 \
|
||||
ELSE session_history_metadata.duration * 1.0 END) * 100) AS percent_complete',
|
||||
'session_history_media_info.video_decision',
|
||||
|
@ -664,7 +664,8 @@ class DataFactory(object):
|
|||
for id in library_cards:
|
||||
if id.isdigit():
|
||||
try:
|
||||
query = 'SELECT section_id, section_name, section_type, thumb, count, parent_count, child_count ' \
|
||||
query = 'SELECT section_id, section_name, section_type, thumb AS library_thumb, ' \
|
||||
'custom_thumb_url AS custom_thumb, count, parent_count, child_count ' \
|
||||
'FROM library_sections ' \
|
||||
'WHERE section_id = %s ' % id
|
||||
result = monitor_db.select(query)
|
||||
|
@ -673,10 +674,17 @@ class DataFactory(object):
|
|||
return None
|
||||
|
||||
for item in result:
|
||||
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
||||
library_thumb = item['custom_thumb']
|
||||
elif item['library_thumb']:
|
||||
library_thumb = item['library_thumb']
|
||||
else:
|
||||
library_thumb = common.DEFAULT_COVER_THUMB
|
||||
|
||||
library = {'section_id': item['section_id'],
|
||||
'section_name': item['section_name'],
|
||||
'section_type': item['section_type'],
|
||||
'thumb': item['thumb'],
|
||||
'thumb': library_thumb,
|
||||
'count': item['count'],
|
||||
'parent_count': item['parent_count'],
|
||||
'child_count': item['child_count']
|
||||
|
|
|
@ -13,22 +13,61 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
from operator import itemgetter
|
||||
from xml.dom import minidom
|
||||
|
||||
import unicodedata
|
||||
import plexpy
|
||||
import base64
|
||||
import datetime
|
||||
import fnmatch
|
||||
import shutil
|
||||
import time
|
||||
import sys
|
||||
import re
|
||||
import os
|
||||
from functools import wraps
|
||||
from IPy import IP
|
||||
import json
|
||||
import xmltodict
|
||||
import math
|
||||
from operator import itemgetter
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
import unicodedata
|
||||
import urllib, urllib2
|
||||
from xml.dom import minidom
|
||||
import xmltodict
|
||||
|
||||
import plexpy
|
||||
from api2 import API2
|
||||
|
||||
|
||||
def addtoapi(*dargs, **dkwargs):
|
||||
""" Helper decorator that adds function to the API class.
|
||||
is used to reuse as much code as possible
|
||||
|
||||
args:
|
||||
dargs: (string, optional) Used to rename a function
|
||||
|
||||
Example:
|
||||
@addtoapi("i_was_renamed", "im_a_second_alias")
|
||||
@addtoapi()
|
||||
|
||||
"""
|
||||
def rd(function):
|
||||
@wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
return function(*args, **kwargs)
|
||||
|
||||
if dargs:
|
||||
# To rename the function if it sucks.. and
|
||||
# allow compat with old api.
|
||||
for n in dargs:
|
||||
if function.__doc__ and len(function.__doc__):
|
||||
function.__doc__ = function.__doc__.strip()
|
||||
setattr(API2, n, function)
|
||||
return wrapper
|
||||
|
||||
if function.__doc__ and len(function.__doc__):
|
||||
function.__doc__ = function.__doc__.strip()
|
||||
setattr(API2, function.__name__, function)
|
||||
return wrapper
|
||||
|
||||
return rd
|
||||
|
||||
def multikeysort(items, columns):
|
||||
comparers = [((itemgetter(col[1:].strip()), -1) if col.startswith('-') else (itemgetter(col.strip()), 1)) for col in columns]
|
||||
|
@ -173,7 +212,7 @@ def human_duration(s, sig='dhms'):
|
|||
if sig >= 'dh' and h > 0:
|
||||
h = h + 1 if sig == 'dh' and m >= 30 else h
|
||||
hd_list.append(str(h) + ' hrs')
|
||||
|
||||
|
||||
if sig >= 'dhm' and m > 0:
|
||||
m = m + 1 if sig == 'dhm' and s >= 30 else m
|
||||
hd_list.append(str(m) + ' mins')
|
||||
|
@ -341,7 +380,7 @@ def split_string(mystring, splitvar=','):
|
|||
|
||||
def create_https_certificates(ssl_cert, ssl_key):
|
||||
"""
|
||||
Create a pair of self-signed HTTPS certificares and store in them in
|
||||
Create a self-signed HTTPS certificate and store in it in
|
||||
'ssl_cert' and 'ssl_key'. Method assumes pyOpenSSL is installed.
|
||||
|
||||
This code is stolen from SickBeard (http://github.com/midgetspy/Sick-Beard).
|
||||
|
@ -350,24 +389,24 @@ def create_https_certificates(ssl_cert, ssl_key):
|
|||
from plexpy import logger
|
||||
|
||||
from OpenSSL import crypto
|
||||
from certgen import createKeyPair, createCertRequest, createCertificate, \
|
||||
TYPE_RSA, serial
|
||||
from certgen import createKeyPair, createSelfSignedCertificate, TYPE_RSA
|
||||
|
||||
# Create the CA Certificate
|
||||
cakey = createKeyPair(TYPE_RSA, 2048)
|
||||
careq = createCertRequest(cakey, CN="Certificate Authority")
|
||||
cacert = createCertificate(careq, (careq, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
serial = int(time.time())
|
||||
domains = ['DNS:' + d.strip() for d in plexpy.CONFIG.HTTPS_DOMAIN.split(',') if d]
|
||||
ips = ['IP:' + d.strip() for d in plexpy.CONFIG.HTTPS_IP.split(',') if d]
|
||||
altNames = ','.join(domains + ips)
|
||||
|
||||
# Create the self-signed PlexPy certificate
|
||||
logger.debug(u"Generating self-signed SSL certificate.")
|
||||
pkey = createKeyPair(TYPE_RSA, 2048)
|
||||
req = createCertRequest(pkey, CN="PlexPy")
|
||||
cert = createCertificate(req, (cacert, cakey), serial, (0, 60 * 60 * 24 * 365 * 10)) # ten years
|
||||
cert = createSelfSignedCertificate(("PlexPy", pkey), serial, (0, 60 * 60 * 24 * 365 * 10), altNames) # ten years
|
||||
|
||||
# Save the key and certificate to disk
|
||||
try:
|
||||
with open(ssl_key, "w") as fp:
|
||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
with open(ssl_cert, "w") as fp:
|
||||
fp.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
|
||||
with open(ssl_key, "w") as fp:
|
||||
fp.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey))
|
||||
except IOError as e:
|
||||
logger.error("Error creating SSL key and certificate: %s", e)
|
||||
return False
|
||||
|
@ -455,4 +494,68 @@ def sanitize(string):
|
|||
if string:
|
||||
return unicode(string).replace('<','<').replace('>','>')
|
||||
else:
|
||||
return ''
|
||||
return ''
|
||||
|
||||
def is_ip_public(host):
|
||||
ip_address = get_ip(host)
|
||||
ip = IP(ip_address)
|
||||
if ip.iptype() == 'PUBLIC':
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_ip(host):
|
||||
from plexpy import logger
|
||||
ip_address = ''
|
||||
try:
|
||||
socket.inet_aton(host)
|
||||
ip_address = host
|
||||
except socket.error:
|
||||
try:
|
||||
ip_address = socket.gethostbyname(host)
|
||||
logger.debug(u"IP Checker :: Resolved %s to %s." % (host, ip_address))
|
||||
except:
|
||||
logger.error(u"IP Checker :: Bad IP or hostname provided.")
|
||||
|
||||
return ip_address
|
||||
|
||||
# Taken from SickRage
|
||||
def anon_url(*url):
|
||||
"""
|
||||
Return a URL string consisting of the Anonymous redirect URL and an arbitrary number of values appended.
|
||||
"""
|
||||
return '' if None in url else '%s%s' % (plexpy.CONFIG.ANON_REDIRECT, ''.join(str(s) for s in url))
|
||||
|
||||
def uploadToImgur(imgPath, imgTitle=''):
|
||||
from plexpy import logger
|
||||
|
||||
client_id = '743b1a443ccd2b0'
|
||||
img_url = ''
|
||||
|
||||
try:
|
||||
with open(imgPath, 'rb') as imgFile:
|
||||
img = imgFile.read()
|
||||
except IOError as e:
|
||||
logger.error(u"PlexPy Helpers :: Unable to read image file for Imgur: %s" % e)
|
||||
return img_url
|
||||
|
||||
headers = {'Authorization': 'Client-ID %s' % client_id}
|
||||
data = {'type': 'base64',
|
||||
'image': base64.b64encode(img)}
|
||||
if imgTitle:
|
||||
data['title'] = imgTitle
|
||||
data['name'] = imgTitle + '.jpg'
|
||||
|
||||
request = urllib2.Request('https://api.imgur.com/3/image', headers=headers, data=urllib.urlencode(data))
|
||||
response = urllib2.urlopen(request)
|
||||
response = json.loads(response.read())
|
||||
|
||||
if response.get('status') == 200:
|
||||
logger.debug(u"PlexPy Helpers :: Image uploaded to Imgur.")
|
||||
img_url = response.get('data').get('link', '')
|
||||
elif response.get('status') >= 400 and response.get('status') < 500:
|
||||
logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur: %s" % response.reason)
|
||||
else:
|
||||
logger.warn(u"PlexPy Helpers :: Unable to upload image to Imgur.")
|
||||
|
||||
return img_url
|
|
@ -16,10 +16,10 @@
|
|||
# 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
|
||||
from httplib import HTTPSConnection
|
||||
from httplib import HTTPConnection
|
||||
import ssl
|
||||
from plexpy import logger, helpers
|
||||
|
||||
|
||||
class HTTPHandler(object):
|
||||
|
|
|
@ -133,6 +133,9 @@ class Libraries(object):
|
|||
'library_sections.custom_thumb_url AS custom_thumb',
|
||||
'library_sections.art',
|
||||
'COUNT(session_history.id) AS plays',
|
||||
'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \
|
||||
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
|
||||
session_history.paused_counter END) AS duration',
|
||||
'MAX(session_history.started) AS last_accessed',
|
||||
'MAX(session_history.id) AS id',
|
||||
'session_history_metadata.full_title AS last_played',
|
||||
|
@ -200,6 +203,7 @@ class Libraries(object):
|
|||
'library_thumb': library_thumb,
|
||||
'library_art': item['art'],
|
||||
'plays': item['plays'],
|
||||
'duration': item['duration'],
|
||||
'last_accessed': item['last_accessed'],
|
||||
'id': item['id'],
|
||||
'last_played': item['last_played'],
|
||||
|
@ -536,51 +540,27 @@ class Libraries(object):
|
|||
def get_details(self, section_id=None):
|
||||
from plexpy import pmsconnect
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
default_return = {'section_id': None,
|
||||
'section_name': 'Local',
|
||||
'section_type': '',
|
||||
'library_thumb': common.DEFAULT_COVER_THUMB,
|
||||
'library_art': '',
|
||||
'count': 0,
|
||||
'parent_count': 0,
|
||||
'child_count': 0,
|
||||
'do_notify': 0,
|
||||
'do_notify_created': 0,
|
||||
'keep_history': 0
|
||||
}
|
||||
|
||||
try:
|
||||
if section_id:
|
||||
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
||||
'do_notify, do_notify_created, keep_history ' \
|
||||
'FROM library_sections ' \
|
||||
'WHERE section_id = ? '
|
||||
result = monitor_db.select(query, args=[section_id])
|
||||
else:
|
||||
result = []
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e)
|
||||
result = []
|
||||
if not section_id:
|
||||
return default_return
|
||||
|
||||
if result:
|
||||
library_details = {}
|
||||
for item in result:
|
||||
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
||||
library_thumb = item['custom_thumb']
|
||||
elif item['library_thumb']:
|
||||
library_thumb = item['library_thumb']
|
||||
else:
|
||||
library_thumb = common.DEFAULT_COVER_THUMB
|
||||
def get_library_details(section_id=section_id):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
library_details = {'section_id': item['section_id'],
|
||||
'section_name': item['section_name'],
|
||||
'section_type': item['section_type'],
|
||||
'library_thumb': library_thumb,
|
||||
'library_art': item['art'],
|
||||
'count': item['count'],
|
||||
'parent_count': item['parent_count'],
|
||||
'child_count': item['child_count'],
|
||||
'do_notify': item['do_notify'],
|
||||
'do_notify_created': item['do_notify_created'],
|
||||
'keep_history': item['keep_history']
|
||||
}
|
||||
return library_details
|
||||
else:
|
||||
logger.warn(u"PlexPy Libraries :: Unable to retrieve library from local database. Requesting library list refresh.")
|
||||
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
|
||||
pmsconnect.refresh_libraries()
|
||||
try:
|
||||
if section_id:
|
||||
if str(section_id).isdigit():
|
||||
query = 'SELECT section_id, section_name, section_type, count, parent_count, child_count, ' \
|
||||
'thumb AS library_thumb, custom_thumb_url AS custom_thumb, art, ' \
|
||||
'do_notify, do_notify_created, keep_history ' \
|
||||
|
@ -589,12 +569,12 @@ class Libraries(object):
|
|||
result = monitor_db.select(query, args=[section_id])
|
||||
else:
|
||||
result = []
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Libraries :: Unable to execute database query for get_details: %s." % e)
|
||||
result = []
|
||||
|
||||
library_details = {}
|
||||
if result:
|
||||
library_details = {}
|
||||
for item in result:
|
||||
if item['custom_thumb'] and item['custom_thumb'] != item['library_thumb']:
|
||||
library_thumb = item['custom_thumb']
|
||||
|
@ -615,22 +595,28 @@ class Libraries(object):
|
|||
'do_notify_created': item['do_notify_created'],
|
||||
'keep_history': item['keep_history']
|
||||
}
|
||||
return library_details
|
||||
|
||||
library_details = get_library_details(section_id=section_id)
|
||||
|
||||
if library_details:
|
||||
return library_details
|
||||
|
||||
else:
|
||||
logger.warn(u"PlexPy Libraries :: Unable to retrieve library from local database. Requesting library list refresh.")
|
||||
# Let's first refresh the libraries list to make sure the library isn't newly added and not in the db yet
|
||||
pmsconnect.refresh_libraries()
|
||||
|
||||
library_details = get_library_details(section_id=section_id)
|
||||
|
||||
if library_details:
|
||||
return library_details
|
||||
|
||||
else:
|
||||
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Returning 'Local' library.")
|
||||
# If there is no library data we must return something
|
||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||
return {'section_id': None,
|
||||
'section_name': 'Local',
|
||||
'section_type': '',
|
||||
'library_thumb': common.DEFAULT_COVER_THUMB,
|
||||
'library_art': '',
|
||||
'count': 0,
|
||||
'parent_count': 0,
|
||||
'child_count': 0,
|
||||
'do_notify': 0,
|
||||
'do_notify_created': 0,
|
||||
'keep_history': 0
|
||||
}
|
||||
# Use "Local" library to retain compatibility with PlexWatch database value
|
||||
return default_return
|
||||
|
||||
def get_watch_time_stats(self, section_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
|
|
@ -18,10 +18,14 @@ import re
|
|||
import os
|
||||
import plexpy
|
||||
|
||||
def get_log_tail(window=20, parsed=True):
|
||||
def get_log_tail(window=20, parsed=True, log_type="server"):
|
||||
|
||||
if plexpy.CONFIG.PMS_LOGS_FOLDER:
|
||||
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Server.log')
|
||||
log_file = ""
|
||||
if log_type == "server":
|
||||
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Server.log')
|
||||
elif log_type == "scanner":
|
||||
log_file = os.path.join(plexpy.CONFIG.PMS_LOGS_FOLDER, 'Plex Media Scanner.log')
|
||||
else:
|
||||
return []
|
||||
|
||||
|
|
|
@ -14,9 +14,11 @@
|
|||
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
import arrow
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import arrow
|
||||
import urllib
|
||||
|
||||
from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect
|
||||
import plexpy
|
||||
|
@ -49,11 +51,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
if agent['on_play'] and notify_action == 'play':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -62,11 +65,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT):
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
||||
|
@ -74,11 +78,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
||||
|
@ -86,18 +91,19 @@ def notify(stream_data=None, notify_action=None):
|
|||
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
||||
elif agent['on_buffer'] and notify_action == 'buffer':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
|
@ -113,11 +119,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
if not any(d['agent_id'] == agent['id'] for d in notify_states):
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -128,11 +135,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
if not notify_state['on_watched'] and (notify_state['agent_id'] == agent['id']):
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -143,11 +151,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
if agent['on_play'] and notify_action == 'play':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -155,11 +164,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
elif agent['on_stop'] and notify_action == 'stop':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -167,11 +177,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
elif agent['on_pause'] and notify_action == 'pause':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -179,11 +190,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
elif agent['on_resume'] and notify_action == 'resume':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -191,11 +203,12 @@ def notify(stream_data=None, notify_action=None):
|
|||
elif agent['on_buffer'] and notify_action == 'buffer':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(session=stream_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
|
||||
|
@ -215,11 +228,12 @@ def notify_timeline(timeline_data=None, notify_action=None):
|
|||
if agent['on_created'] and notify_action == 'created':
|
||||
# Build and send notification
|
||||
notify_strings = build_notify_text(timeline=timeline_data, state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
script_args=notify_strings[2])
|
||||
script_args=notify_strings[2],
|
||||
metadata=notify_strings[3])
|
||||
# Set the notification state in the db
|
||||
set_notify_state(session=timeline_data, state=notify_action, agent_info=agent)
|
||||
|
||||
|
@ -228,7 +242,7 @@ def notify_timeline(timeline_data=None, notify_action=None):
|
|||
if agent['on_extdown'] and notify_action == 'extdown':
|
||||
# Build and send notification
|
||||
notify_strings = build_server_notify_text(state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
|
@ -236,7 +250,7 @@ def notify_timeline(timeline_data=None, notify_action=None):
|
|||
if agent['on_intdown'] and notify_action == 'intdown':
|
||||
# Build and send notification
|
||||
notify_strings = build_server_notify_text(state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
|
@ -244,7 +258,7 @@ def notify_timeline(timeline_data=None, notify_action=None):
|
|||
if agent['on_extup'] and notify_action == 'extup':
|
||||
# Build and send notification
|
||||
notify_strings = build_server_notify_text(state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
|
@ -252,7 +266,7 @@ def notify_timeline(timeline_data=None, notify_action=None):
|
|||
if agent['on_intup'] and notify_action == 'intup':
|
||||
# Build and send notification
|
||||
notify_strings = build_server_notify_text(state=notify_action)
|
||||
notifiers.send_notification(config_id=agent['id'],
|
||||
notifiers.send_notification(agent_id=agent['id'],
|
||||
subject=notify_strings[0],
|
||||
body=notify_strings[1],
|
||||
notify_action=notify_action,
|
||||
|
@ -446,10 +460,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
transcode_decision = 'Direct Play'
|
||||
|
||||
if state != 'play':
|
||||
stream_duration = helpers.convert_seconds_to_minutes(
|
||||
time.time() -
|
||||
helpers.cast_to_int(session.get('started', 0)) -
|
||||
helpers.cast_to_int(session.get('paused_counter', 0)))
|
||||
stream_duration = int((time.time() -
|
||||
helpers.cast_to_int(session.get('started', 0)) -
|
||||
helpers.cast_to_int(session.get('paused_counter', 0))) / 60)
|
||||
else:
|
||||
stream_duration = 0
|
||||
|
||||
|
@ -458,6 +471,53 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
progress_percent = helpers.get_percent(view_offset, duration)
|
||||
remaining_duration = duration - view_offset
|
||||
|
||||
# Get media IDs from guid and build URLs
|
||||
if 'imdb://' in metadata['guid']:
|
||||
metadata['imdb_id'] = metadata['guid'].split('imdb://')[1].split('?')[0]
|
||||
metadata['imdb_url'] = 'https://www.imdb.com/title/' + metadata['imdb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/imdb/' + metadata['imdb_id']
|
||||
|
||||
if 'thetvdb://' in metadata['guid']:
|
||||
metadata['thetvdb_id'] = metadata['guid'].split('thetvdb://')[1].split('/')[0]
|
||||
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show'
|
||||
|
||||
elif 'thetvdbdvdorder://' in metadata['guid']:
|
||||
metadata['thetvdb_id'] = metadata['guid'].split('thetvdbdvdorder://')[1].split('/')[0]
|
||||
metadata['thetvdb_url'] = 'https://thetvdb.com/?tab=series&id=' + metadata['thetvdb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tvdb/' + metadata['thetvdb_id'] + '?id_type=show'
|
||||
|
||||
if 'themoviedb://' in metadata['guid']:
|
||||
if metadata['media_type'] == 'movie':
|
||||
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('?')[0]
|
||||
metadata['themoviedb_url'] = 'https://www.themoviedb.org/movie/' + metadata['themoviedb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=movie'
|
||||
|
||||
elif metadata['media_type'] == 'show' or metadata['media_type'] == 'episode':
|
||||
metadata['themoviedb_id'] = metadata['guid'].split('themoviedb://')[1].split('/')[0]
|
||||
metadata['themoviedb_url'] = 'https://www.themoviedb.org/tv/' + metadata['themoviedb_id']
|
||||
metadata['trakt_url'] = 'https://trakt.tv/search/tmdb/' + metadata['themoviedb_id'] + '?id_type=show'
|
||||
|
||||
if 'lastfm://' in metadata['guid']:
|
||||
metadata['lastfm_id'] = metadata['guid'].split('lastfm://')[1].rsplit('/', 1)[0]
|
||||
metadata['lastfm_url'] = 'https://www.last.fm/music/' + metadata['lastfm_id']
|
||||
|
||||
if metadata['media_type'] == 'movie' or metadata['media_type'] == 'show' or metadata['media_type'] == 'artist':
|
||||
thumb = metadata['thumb']
|
||||
elif metadata['media_type'] == 'episode':
|
||||
thumb = metadata['grandparent_thumb']
|
||||
elif metadata['media_type'] == 'track':
|
||||
thumb = metadata['parent_thumb']
|
||||
else:
|
||||
thumb = None
|
||||
|
||||
if thumb:
|
||||
# Retrieve the poster from Plex and cache to file
|
||||
urllib.urlretrieve(plexpy.CONFIG.PMS_URL + thumb + '?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN,
|
||||
os.path.join(plexpy.CONFIG.CACHE_DIR, 'cache-poster.jpg'))
|
||||
# Upload thumb to Imgur and get link
|
||||
metadata['poster_url'] = helpers.uploadToImgur(os.path.join(plexpy.CONFIG.CACHE_DIR, 'cache-poster.jpg'), full_title)
|
||||
|
||||
# Fix metadata params for notify recently added grandparent
|
||||
if state == 'created' and plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
|
||||
show_name = metadata['title']
|
||||
|
@ -512,6 +572,7 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
'transcode_audio_channels': session.get('transcode_audio_channels',''),
|
||||
'session_key': session.get('session_key',''),
|
||||
'user_id': session.get('user_id',''),
|
||||
'machine_id': session.get('machine_id',''),
|
||||
# Metadata parameters
|
||||
'media_type': metadata['media_type'],
|
||||
'title': full_title,
|
||||
|
@ -538,6 +599,15 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
'tagline': metadata['tagline'],
|
||||
'rating': metadata['rating'],
|
||||
'duration': duration,
|
||||
'poster_url': metadata.get('poster_url',''),
|
||||
'imdb_id': metadata.get('imdb_id',''),
|
||||
'imdb_url': metadata.get('imdb_url',''),
|
||||
'thetvdb_id': metadata.get('thetvdb_id',''),
|
||||
'thetvdb_url': metadata.get('thetvdb_url',''),
|
||||
'themoviedb_id': metadata.get('themoviedb_id',''),
|
||||
'themoviedb_url': metadata.get('themoviedb_url',''),
|
||||
'lastfm_url': metadata.get('lastfm_url',''),
|
||||
'trakt_url': metadata.get('trakt_url',''),
|
||||
'section_id': metadata['section_id'],
|
||||
'rating_key': metadata['rating_key'],
|
||||
'parent_rating_key': metadata['parent_rating_key'],
|
||||
|
@ -579,9 +649,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
elif state == 'stop':
|
||||
# Default body text
|
||||
body_text = '%s (%s) has stopped %s' % (session['friendly_name'],
|
||||
|
@ -603,9 +673,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
elif state == 'pause':
|
||||
# Default body text
|
||||
body_text = '%s (%s) has paused %s' % (session['friendly_name'],
|
||||
|
@ -627,9 +697,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
elif state == 'resume':
|
||||
# Default body text
|
||||
body_text = '%s (%s) has resumed %s' % (session['friendly_name'],
|
||||
|
@ -651,9 +721,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
elif state == 'buffer':
|
||||
# Default body text
|
||||
body_text = '%s (%s) is buffering %s' % (session['friendly_name'],
|
||||
|
@ -675,9 +745,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
elif state == 'watched':
|
||||
# Default body text
|
||||
body_text = '%s (%s) has watched %s' % (session['friendly_name'],
|
||||
|
@ -699,9 +769,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
elif state == 'created':
|
||||
# Default body text
|
||||
body_text = '%s was recently added to Plex.' % full_title
|
||||
|
@ -721,9 +791,9 @@ def build_notify_text(session=None, timeline=None, state=None):
|
|||
except:
|
||||
logger.error(u"PlexPy NotificationHandler :: Unable to parse custom notification body. Using fallback.")
|
||||
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return [subject_text, body_text, script_args]
|
||||
return [subject_text, body_text, script_args, metadata]
|
||||
else:
|
||||
return None
|
||||
|
||||
|
|
|
@ -358,59 +358,59 @@ def available_notification_agents():
|
|||
return agents
|
||||
|
||||
|
||||
def get_notification_agent_config(config_id):
|
||||
if str(config_id).isdigit():
|
||||
config_id = int(config_id)
|
||||
def get_notification_agent_config(agent_id):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
|
||||
if config_id == 0:
|
||||
if agent_id == 0:
|
||||
growl = GROWL()
|
||||
return growl.return_config_options()
|
||||
elif config_id == 1:
|
||||
elif agent_id == 1:
|
||||
prowl = PROWL()
|
||||
return prowl.return_config_options()
|
||||
elif config_id == 2:
|
||||
elif agent_id == 2:
|
||||
xbmc = XBMC()
|
||||
return xbmc.return_config_options()
|
||||
elif config_id == 3:
|
||||
elif agent_id == 3:
|
||||
plex = Plex()
|
||||
return plex.return_config_options()
|
||||
elif config_id == 4:
|
||||
elif agent_id == 4:
|
||||
nma = NMA()
|
||||
return nma.return_config_options()
|
||||
elif config_id == 5:
|
||||
elif agent_id == 5:
|
||||
pushalot = PUSHALOT()
|
||||
return pushalot.return_config_options()
|
||||
elif config_id == 6:
|
||||
elif agent_id == 6:
|
||||
pushbullet = PUSHBULLET()
|
||||
return pushbullet.return_config_options()
|
||||
elif config_id == 7:
|
||||
elif agent_id == 7:
|
||||
pushover = PUSHOVER()
|
||||
return pushover.return_config_options()
|
||||
elif config_id == 8:
|
||||
elif agent_id == 8:
|
||||
osx_notify = OSX_NOTIFY()
|
||||
return osx_notify.return_config_options()
|
||||
elif config_id == 9:
|
||||
elif agent_id == 9:
|
||||
boxcar = BOXCAR()
|
||||
return boxcar.return_config_options()
|
||||
elif config_id == 10:
|
||||
elif agent_id == 10:
|
||||
email = Email()
|
||||
return email.return_config_options()
|
||||
elif config_id == 11:
|
||||
elif agent_id == 11:
|
||||
tweet = TwitterNotifier()
|
||||
return tweet.return_config_options()
|
||||
elif config_id == 12:
|
||||
elif agent_id == 12:
|
||||
iftttClient = IFTTT()
|
||||
return iftttClient.return_config_options()
|
||||
elif config_id == 13:
|
||||
elif agent_id == 13:
|
||||
telegramClient = TELEGRAM()
|
||||
return telegramClient.return_config_options()
|
||||
elif config_id == 14:
|
||||
elif agent_id == 14:
|
||||
slackClient = SLACK()
|
||||
return slackClient.return_config_options()
|
||||
elif config_id == 15:
|
||||
elif agent_id == 15:
|
||||
script = Scripts()
|
||||
return script.return_config_options()
|
||||
elif config_id == 16:
|
||||
elif agent_id == 16:
|
||||
facebook = FacebookNotifier()
|
||||
return facebook.return_config_options()
|
||||
else:
|
||||
|
@ -419,61 +419,61 @@ def get_notification_agent_config(config_id):
|
|||
return []
|
||||
|
||||
|
||||
def send_notification(config_id, subject, body, **kwargs):
|
||||
if str(config_id).isdigit():
|
||||
config_id = int(config_id)
|
||||
def send_notification(agent_id, subject, body, **kwargs):
|
||||
if str(agent_id).isdigit():
|
||||
agent_id = int(agent_id)
|
||||
|
||||
if config_id == 0:
|
||||
if agent_id == 0:
|
||||
growl = GROWL()
|
||||
growl.notify(message=body, event=subject)
|
||||
elif config_id == 1:
|
||||
elif agent_id == 1:
|
||||
prowl = PROWL()
|
||||
prowl.notify(message=body, event=subject)
|
||||
elif config_id == 2:
|
||||
elif agent_id == 2:
|
||||
xbmc = XBMC()
|
||||
xbmc.notify(subject=subject, message=body)
|
||||
elif config_id == 3:
|
||||
elif agent_id == 3:
|
||||
plex = Plex()
|
||||
plex.notify(subject=subject, message=body)
|
||||
elif config_id == 4:
|
||||
elif agent_id == 4:
|
||||
nma = NMA()
|
||||
nma.notify(subject=subject, message=body)
|
||||
elif config_id == 5:
|
||||
elif agent_id == 5:
|
||||
pushalot = PUSHALOT()
|
||||
pushalot.notify(message=body, event=subject)
|
||||
elif config_id == 6:
|
||||
elif agent_id == 6:
|
||||
pushbullet = PUSHBULLET()
|
||||
pushbullet.notify(message=body, subject=subject)
|
||||
elif config_id == 7:
|
||||
elif agent_id == 7:
|
||||
pushover = PUSHOVER()
|
||||
pushover.notify(message=body, event=subject)
|
||||
elif config_id == 8:
|
||||
elif agent_id == 8:
|
||||
osx_notify = OSX_NOTIFY()
|
||||
osx_notify.notify(title=subject, text=body)
|
||||
elif config_id == 9:
|
||||
elif agent_id == 9:
|
||||
boxcar = BOXCAR()
|
||||
boxcar.notify(title=subject, message=body)
|
||||
elif config_id == 10:
|
||||
elif agent_id == 10:
|
||||
email = Email()
|
||||
email.notify(subject=subject, message=body)
|
||||
elif config_id == 11:
|
||||
elif agent_id == 11:
|
||||
tweet = TwitterNotifier()
|
||||
tweet.notify(subject=subject, message=body)
|
||||
elif config_id == 12:
|
||||
elif agent_id == 12:
|
||||
iftttClient = IFTTT()
|
||||
iftttClient.notify(subject=subject, message=body)
|
||||
elif config_id == 13:
|
||||
elif agent_id == 13:
|
||||
telegramClient = TELEGRAM()
|
||||
telegramClient.notify(message=body, event=subject)
|
||||
elif config_id == 14:
|
||||
elif agent_id == 14:
|
||||
slackClient = SLACK()
|
||||
slackClient.notify(message=body, event=subject)
|
||||
elif config_id == 15:
|
||||
elif agent_id == 15:
|
||||
scripts = Scripts()
|
||||
scripts.notify(message=body, subject=subject, **kwargs)
|
||||
elif config_id == 16:
|
||||
elif agent_id == 16:
|
||||
facebook = FacebookNotifier()
|
||||
facebook.notify(subject=subject, message=body)
|
||||
facebook.notify(subject=subject, message=body, **kwargs)
|
||||
else:
|
||||
logger.debug(u"PlexPy Notifiers :: Unknown agent id received.")
|
||||
else:
|
||||
|
@ -1169,12 +1169,16 @@ class TwitterNotifier(object):
|
|||
self.access_token_secret = plexpy.CONFIG.TWITTER_ACCESS_TOKEN_SECRET
|
||||
self.consumer_key = plexpy.CONFIG.TWITTER_CONSUMER_KEY
|
||||
self.consumer_secret = plexpy.CONFIG.TWITTER_CONSUMER_SECRET
|
||||
self.incl_subject = plexpy.CONFIG.TWITTER_INCL_SUBJECT
|
||||
|
||||
def notify(self, subject, message):
|
||||
if not subject or not message:
|
||||
return
|
||||
else:
|
||||
self._send_tweet(subject + ': ' + message)
|
||||
if self.incl_subject:
|
||||
self._send_tweet(subject + ': ' + message)
|
||||
else:
|
||||
self._send_tweet(message)
|
||||
|
||||
def test_notify(self):
|
||||
return self._send_tweet("This is a test notification from PlexPy at " + helpers.now())
|
||||
|
@ -1284,6 +1288,12 @@ class TwitterNotifier(object):
|
|||
'name': 'twitter_access_token_secret',
|
||||
'description': 'Your Twitter access token secret.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'twitter_incl_subject',
|
||||
'description': 'Include the subject line in the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -1628,6 +1638,7 @@ class TELEGRAM(object):
|
|||
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
|
||||
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
|
||||
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
|
||||
self.incl_subject = plexpy.CONFIG.TELEGRAM_INCL_SUBJECT
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Telegram', options)
|
||||
|
@ -1638,8 +1649,13 @@ class TELEGRAM(object):
|
|||
|
||||
http_handler = HTTPSConnection("api.telegram.org")
|
||||
|
||||
if self.incl_subject:
|
||||
text = event.encode('utf-8') + ': ' + message.encode("utf-8")
|
||||
else:
|
||||
text = message.encode("utf-8")
|
||||
|
||||
data = {'chat_id': self.chat_id,
|
||||
'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
|
||||
'text': text}
|
||||
|
||||
http_handler.request("POST",
|
||||
"/bot%s/%s" % (self.bot_token, "sendMessage"),
|
||||
|
@ -1682,6 +1698,12 @@ class TELEGRAM(object):
|
|||
'name': 'telegram_chat_id',
|
||||
'description': 'Your Telegram Chat ID, Group ID, or @channelusername. Contact <a href="http://telegram.me/myidbot" target="_blank">@myidbot</a> on Telegram to get an ID.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'telegram_incl_subject',
|
||||
'description': 'Include the subject line in the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -1698,6 +1720,7 @@ class SLACK(object):
|
|||
self.channel = plexpy.CONFIG.SLACK_CHANNEL
|
||||
self.username = plexpy.CONFIG.SLACK_USERNAME
|
||||
self.icon_emoji = plexpy.CONFIG.SLACK_ICON_EMOJI
|
||||
self.incl_subject = plexpy.CONFIG.SLACK_INCL_SUBJECT
|
||||
|
||||
def conf(self, options):
|
||||
return cherrypy.config['config'].get('Slack', options)
|
||||
|
@ -1707,7 +1730,12 @@ class SLACK(object):
|
|||
return
|
||||
http_handler = HTTPSConnection("hooks.slack.com")
|
||||
|
||||
data = {'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
|
||||
if self.incl_subject:
|
||||
text = event.encode('utf-8') + ': ' + message.encode("utf-8")
|
||||
else:
|
||||
text = message.encode("utf-8")
|
||||
|
||||
data = {'text': text}
|
||||
if self.channel != '': data['channel'] = self.channel
|
||||
if self.username != '': data['username'] = self.username
|
||||
if self.icon_emoji != '':
|
||||
|
@ -1745,10 +1773,10 @@ class SLACK(object):
|
|||
return self.notify('Main Screen Activate', 'Test Message')
|
||||
|
||||
def return_config_options(self):
|
||||
config_option = [{'label': 'Slack Hook',
|
||||
config_option = [{'label': 'Slack Webhook URL',
|
||||
'value': self.slack_hook,
|
||||
'name': 'slack_hook',
|
||||
'description': 'Your Slack incoming webhook.',
|
||||
'description': 'Your Slack incoming webhook URL.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Slack Channel',
|
||||
|
@ -1768,6 +1796,12 @@ class SLACK(object):
|
|||
'description': 'The icon you wish to show, use Slack emoji or image url. Leave blank for webhook integration default.',
|
||||
'name': 'slack_icon_emoji',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'slack_incl_subject',
|
||||
'description': 'Include the subject line in the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -2035,15 +2069,21 @@ class FacebookNotifier(object):
|
|||
|
||||
def __init__(self):
|
||||
self.redirect_uri = plexpy.CONFIG.FACEBOOK_REDIRECT_URI
|
||||
self.access_token = plexpy.CONFIG.FACEBOOK_TOKEN
|
||||
self.app_id = plexpy.CONFIG.FACEBOOK_APP_ID
|
||||
self.app_secret = plexpy.CONFIG.FACEBOOK_APP_SECRET
|
||||
self.group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
||||
self.incl_poster = plexpy.CONFIG.FACEBOOK_INCL_POSTER
|
||||
self.incl_subject = plexpy.CONFIG.FACEBOOK_INCL_SUBJECT
|
||||
|
||||
def notify(self, subject, message):
|
||||
def notify(self, subject, message, **kwargs):
|
||||
if not subject or not message:
|
||||
return
|
||||
else:
|
||||
self._post_facebook(subject + ': ' + message)
|
||||
if self.incl_subject:
|
||||
self._post_facebook(subject + ': ' + message, **kwargs)
|
||||
else:
|
||||
self._post_facebook(message, **kwargs)
|
||||
|
||||
def test_notify(self):
|
||||
return self._post_facebook(u"PlexPy Notifiers :: This is a test notification from PlexPy at " + helpers.now())
|
||||
|
@ -2079,15 +2119,51 @@ class FacebookNotifier(object):
|
|||
|
||||
return True
|
||||
|
||||
def _post_facebook(self, message=None):
|
||||
access_token = plexpy.CONFIG.FACEBOOK_TOKEN
|
||||
group_id = plexpy.CONFIG.FACEBOOK_GROUP
|
||||
def _post_facebook(self, message=None, **kwargs):
|
||||
if self.group_id:
|
||||
api = facebook.GraphAPI(access_token=self.access_token, version='2.5')
|
||||
|
||||
if group_id:
|
||||
api = facebook.GraphAPI(access_token=access_token, version='2.5')
|
||||
attachment = {}
|
||||
|
||||
if self.incl_poster and 'metadata' in kwargs:
|
||||
metadata = kwargs['metadata']
|
||||
poster_url = metadata.get('poster_url','')
|
||||
|
||||
if poster_url:
|
||||
if metadata['media_type'] == 'movie' or metadata['media_type'] == 'show':
|
||||
title = metadata['title']
|
||||
subtitle = metadata['year']
|
||||
rating_key = metadata['rating_key']
|
||||
|
||||
elif metadata['media_type'] == 'episode':
|
||||
title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||
subtitle = 'S%s %s E%s' % (metadata['parent_media_index'],
|
||||
'\xc2\xb7'.decode('utf8'),
|
||||
metadata['media_index'])
|
||||
rating_key = metadata['rating_key']
|
||||
|
||||
elif metadata['media_type'] == 'artist':
|
||||
title = metadata['title']
|
||||
subtitle = ''
|
||||
rating_key = metadata['rating_key']
|
||||
|
||||
elif metadata['media_type'] == 'track':
|
||||
title = '%s - %s' % (metadata['grandparent_title'], metadata['title'])
|
||||
subtitle = metadata['parent_title']
|
||||
rating_key = metadata['parent_rating_key']
|
||||
|
||||
caption = 'View in Plex Web.'
|
||||
|
||||
# Build Facebook post attachment
|
||||
attachment['link'] = 'http://app.plex.tv/web/app#!/server/' + plexpy.CONFIG.PMS_IDENTIFIER + \
|
||||
'/details/%2Flibrary%2Fmetadata%2F' + rating_key
|
||||
attachment['picture'] = poster_url
|
||||
attachment['name'] = title
|
||||
attachment['description'] = subtitle
|
||||
attachment['caption'] = caption
|
||||
|
||||
try:
|
||||
api.put_wall_post(profile_id=group_id, message=message)
|
||||
api.put_wall_post(profile_id=self.group_id, message=message, attachment=attachment)
|
||||
logger.info(u"PlexPy Notifiers :: Facebook notification sent.")
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Notifiers :: Error sending Facebook post: %s" % e)
|
||||
|
@ -2143,6 +2219,18 @@ class FacebookNotifier(object):
|
|||
'name': 'facebook_group',
|
||||
'description': 'Your Facebook Group ID.',
|
||||
'input_type': 'text'
|
||||
},
|
||||
{'label': 'Include Poster Image',
|
||||
'value': self.incl_poster,
|
||||
'name': 'facebook_incl_poster',
|
||||
'description': 'Include a poster and link in the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
},
|
||||
{'label': 'Include Subject Line',
|
||||
'value': self.incl_subject,
|
||||
'name': 'facebook_incl_subject',
|
||||
'description': 'Include the subject line in the notifications.',
|
||||
'input_type': 'checkbox'
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -86,7 +86,7 @@ def get_real_pms_url():
|
|||
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
||||
plexpy.CONFIG.write()
|
||||
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
||||
if not plexpy.CONFIG.PMS_IS_REMOTE and item['local'] == '1':
|
||||
if not plexpy.CONFIG.PMS_IS_REMOTE and item['local'] == '1' and 'plex.direct' in item['uri']:
|
||||
plexpy.CONFIG.__setattr__('PMS_URL', item['uri'])
|
||||
plexpy.CONFIG.write()
|
||||
logger.info(u"PlexPy PlexTV :: Server URL retrieved.")
|
||||
|
@ -493,6 +493,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 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'),
|
||||
'label': helpers.get_xml_attr(d, 'name'),
|
||||
|
|
|
@ -19,10 +19,11 @@ from urlparse import urlparse
|
|||
import plexpy
|
||||
import urllib2
|
||||
|
||||
|
||||
def get_server_friendly_name():
|
||||
logger.info(u"PlexPy Pmsconnect :: Requesting name from server...")
|
||||
server_name = PmsConnect().get_server_pref(pref='FriendlyName')
|
||||
|
||||
|
||||
# If friendly name is blank
|
||||
if not server_name:
|
||||
servers_info = PmsConnect().get_servers_info()
|
||||
|
@ -30,7 +31,7 @@ def get_server_friendly_name():
|
|||
if server['machine_identifier'] == plexpy.CONFIG.PMS_IDENTIFIER:
|
||||
server_name = server['name']
|
||||
break
|
||||
|
||||
|
||||
if server_name and server_name != plexpy.CONFIG.PMS_NAME:
|
||||
plexpy.CONFIG.__setattr__('PMS_NAME', server_name)
|
||||
plexpy.CONFIG.write()
|
||||
|
@ -38,6 +39,7 @@ def get_server_friendly_name():
|
|||
|
||||
return server_name
|
||||
|
||||
|
||||
def refresh_libraries():
|
||||
logger.info(u"PlexPy Pmsconnect :: Requesting libraries list refresh...")
|
||||
|
||||
|
@ -71,7 +73,6 @@ def refresh_libraries():
|
|||
|
||||
library_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()
|
||||
|
@ -206,7 +207,7 @@ class PmsConnect(object):
|
|||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
|
||||
return request
|
||||
|
||||
def get_childrens_list(self, rating_key='', output_format=''):
|
||||
|
@ -223,7 +224,7 @@ class PmsConnect(object):
|
|||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
output_format=output_format)
|
||||
|
||||
|
||||
return request
|
||||
|
||||
def get_server_list(self, output_format=''):
|
||||
|
@ -300,7 +301,7 @@ class PmsConnect(object):
|
|||
"""
|
||||
count = '&X-Plex-Container-Size=' + count if count else ''
|
||||
|
||||
uri = '/library/sections/' + section_id + '/' + list_type +'?X-Plex-Container-Start=0' + count + sort_type
|
||||
uri = '/library/sections/' + section_id + '/' + list_type + '?X-Plex-Container-Start=0' + count + sort_type
|
||||
request = self.request_handler.make_request(uri=uri,
|
||||
proto=self.protocol,
|
||||
request_type='GET',
|
||||
|
@ -835,7 +836,7 @@ class PmsConnect(object):
|
|||
metadata = self.get_metadata_details(str(child_rating_key), get_media_info)
|
||||
if metadata:
|
||||
metadata_list.append(metadata['metadata'])
|
||||
|
||||
|
||||
elif get_children and a.getElementsByTagName('Directory'):
|
||||
dir_main = a.getElementsByTagName('Directory')
|
||||
metadata_main = [d for d in dir_main if helpers.get_xml_attr(d, 'ratingKey')]
|
||||
|
@ -844,7 +845,7 @@ class PmsConnect(object):
|
|||
metadata = self.get_metadata_children_details(str(child_rating_key), get_children, get_media_info)
|
||||
if metadata:
|
||||
metadata_list.extend(metadata['metadata'])
|
||||
|
||||
|
||||
output = {'metadata': metadata_list}
|
||||
return output
|
||||
|
||||
|
@ -892,7 +893,7 @@ class PmsConnect(object):
|
|||
metadata['section_type'] = 'track'
|
||||
|
||||
metadata_list = {'metadata': metadata}
|
||||
|
||||
|
||||
return metadata_list
|
||||
|
||||
def get_current_activity(self):
|
||||
|
@ -995,7 +996,7 @@ class PmsConnect(object):
|
|||
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
||||
|
||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
|
@ -1117,7 +1118,7 @@ class PmsConnect(object):
|
|||
|
||||
if helpers.get_xml_attr(session, 'type') == 'episode':
|
||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
|
@ -1175,7 +1176,7 @@ class PmsConnect(object):
|
|||
|
||||
elif helpers.get_xml_attr(session, 'type') == 'movie':
|
||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
|
@ -1233,7 +1234,7 @@ class PmsConnect(object):
|
|||
|
||||
elif helpers.get_xml_attr(session, 'type') == 'clip':
|
||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
|
@ -1324,7 +1325,7 @@ class PmsConnect(object):
|
|||
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
|
||||
|
||||
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'section_id': helpers.get_xml_attr(session, 'librarySectionID'),
|
||||
'media_index': helpers.get_xml_attr(session, 'index'),
|
||||
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
|
||||
'art': helpers.get_xml_attr(session, 'art'),
|
||||
|
@ -1409,7 +1410,7 @@ class PmsConnect(object):
|
|||
children_list = {'children_count': '0',
|
||||
'children_list': []
|
||||
}
|
||||
return parent_list
|
||||
return children_list
|
||||
|
||||
result_data = []
|
||||
|
||||
|
@ -1556,7 +1557,7 @@ class PmsConnect(object):
|
|||
'title': helpers.get_xml_attr(xml_head[0], 'title1'),
|
||||
'libraries_list': libraries_list
|
||||
}
|
||||
|
||||
|
||||
return output
|
||||
|
||||
def get_library_children_details(self, section_id='', section_type='', list_type='all', count='', rating_key='', get_media_info=False):
|
||||
|
@ -1613,15 +1614,15 @@ class PmsConnect(object):
|
|||
if a.getAttribute('size') == '0':
|
||||
logger.debug(u"PlexPy Pmsconnect :: No library data.")
|
||||
childern_list = {'library_count': '0',
|
||||
'childern_list': []
|
||||
}
|
||||
'childern_list': []
|
||||
}
|
||||
return childern_list
|
||||
|
||||
if rating_key:
|
||||
library_count = helpers.get_xml_attr(xml_head[0], 'size')
|
||||
else:
|
||||
library_count = helpers.get_xml_attr(xml_head[0], 'totalSize')
|
||||
|
||||
|
||||
# Get show/season info from xml_head
|
||||
|
||||
item_main = []
|
||||
|
@ -1673,7 +1674,7 @@ class PmsConnect(object):
|
|||
output = {'library_count': library_count,
|
||||
'childern_list': childern_list
|
||||
}
|
||||
|
||||
|
||||
return output
|
||||
|
||||
def get_library_details(self):
|
||||
|
@ -1788,7 +1789,7 @@ class PmsConnect(object):
|
|||
except Exception as e:
|
||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_search_result_details: %s." % e)
|
||||
return []
|
||||
|
||||
|
||||
search_results_count = 0
|
||||
search_results_list = {'movie': [],
|
||||
'show': [],
|
||||
|
@ -1806,8 +1807,8 @@ class PmsConnect(object):
|
|||
if totalSize == 0:
|
||||
logger.debug(u"PlexPy Pmsconnect :: No search results.")
|
||||
search_results_list = {'results_count': search_results_count,
|
||||
'results_list': []
|
||||
}
|
||||
'results_list': []
|
||||
}
|
||||
return search_results_list
|
||||
|
||||
for a in xml_head:
|
||||
|
@ -1912,7 +1913,7 @@ class PmsConnect(object):
|
|||
if a.getAttribute('size'):
|
||||
if a.getAttribute('size') == '0':
|
||||
return {}
|
||||
|
||||
|
||||
title = helpers.get_xml_attr(a, 'title2')
|
||||
|
||||
if a.getElementsByTagName('Directory'):
|
||||
|
@ -1957,34 +1958,33 @@ class PmsConnect(object):
|
|||
if child_rating_key:
|
||||
key = int(child_index)
|
||||
children.update({key: {'rating_key': int(child_rating_key)}})
|
||||
|
||||
|
||||
key = int(parent_index) if match_type == 'index' else parent_title
|
||||
parents.update({key:
|
||||
parents.update({key:
|
||||
{'rating_key': int(parent_rating_key),
|
||||
'children': children}
|
||||
})
|
||||
|
||||
|
||||
key = 0 if match_type == 'index' else title
|
||||
key_list = {key:
|
||||
{'rating_key': int(rating_key),
|
||||
'children': parents },
|
||||
'section_id': section_id,
|
||||
'library_name': library_name
|
||||
}
|
||||
|
||||
key_list = {key: {'rating_key': int(rating_key),
|
||||
'children': parents},
|
||||
'section_id': section_id,
|
||||
'library_name': library_name
|
||||
}
|
||||
|
||||
return key_list
|
||||
|
||||
def get_server_response(self):
|
||||
# Refresh Plex remote access port mapping first
|
||||
self.put_refresh_reachability()
|
||||
account_data = self.get_account(output_format='xml')
|
||||
|
||||
|
||||
try:
|
||||
xml_head = account_data.getElementsByTagName('MyPlex')
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Pmsconnect :: Unable to parse XML for get_server_response: %s." % e)
|
||||
return None
|
||||
|
||||
|
||||
server_response = {}
|
||||
|
||||
for a in xml_head:
|
||||
|
@ -1993,5 +1993,5 @@ class PmsConnect(object):
|
|||
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
|
||||
'public_port': helpers.get_xml_attr(a, 'publicPort')
|
||||
}
|
||||
|
||||
return server_response
|
||||
|
||||
return server_response
|
||||
|
|
|
@ -32,6 +32,9 @@ class Users(object):
|
|||
'users.thumb AS user_thumb',
|
||||
'users.custom_avatar_url AS custom_thumb',
|
||||
'COUNT(session_history.id) AS plays',
|
||||
'SUM(CASE WHEN session_history.stopped > 0 THEN (session_history.stopped - session_history.started) \
|
||||
ELSE 0 END) - SUM(CASE WHEN session_history.paused_counter IS NULL THEN 0 ELSE \
|
||||
session_history.paused_counter END) AS duration',
|
||||
'MAX(session_history.started) AS last_seen',
|
||||
'MAX(session_history.id) AS id',
|
||||
'session_history_metadata.full_title AS last_played',
|
||||
|
@ -100,6 +103,7 @@ class Users(object):
|
|||
'friendly_name': item['friendly_name'],
|
||||
'user_thumb': user_thumb,
|
||||
'plays': item['plays'],
|
||||
'duration': item['duration'],
|
||||
'last_seen': item['last_seen'],
|
||||
'last_played': item['last_played'],
|
||||
'id': item['id'],
|
||||
|
@ -241,58 +245,24 @@ class Users(object):
|
|||
def get_details(self, user_id=None, user=None):
|
||||
from plexpy import plextv
|
||||
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
||||
'FROM users ' \
|
||||
'WHERE user_id = ? '
|
||||
result = monitor_db.select(query, args=[user_id])
|
||||
elif user:
|
||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
'email, is_home_user, is_allow_sync, is_restricted, do_notify, keep_history ' \
|
||||
'FROM users ' \
|
||||
'WHERE username = ? '
|
||||
result = monitor_db.select(query, args=[user])
|
||||
else:
|
||||
result = []
|
||||
except Exception as e:
|
||||
logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e)
|
||||
result = []
|
||||
default_return = {'user_id': None,
|
||||
'username': 'Local',
|
||||
'friendly_name': 'Local',
|
||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||
'email': '',
|
||||
'is_home_user': 0,
|
||||
'is_allow_sync': 0,
|
||||
'is_restricted': 0,
|
||||
'do_notify': 0,
|
||||
'keep_history': 0
|
||||
}
|
||||
|
||||
if result:
|
||||
user_details = {}
|
||||
for item in result:
|
||||
if item['friendly_name']:
|
||||
friendly_name = item['friendly_name']
|
||||
else:
|
||||
friendly_name = item['username']
|
||||
if not user_id and not user:
|
||||
return default_return
|
||||
|
||||
if item['custom_thumb'] and item['custom_thumb'] != item['user_thumb']:
|
||||
user_thumb = item['custom_thumb']
|
||||
elif item['user_thumb']:
|
||||
user_thumb = item['user_thumb']
|
||||
else:
|
||||
user_thumb = common.DEFAULT_USER_THUMB
|
||||
def get_user_details(user_id=user_id, user=user):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
user_details = {'user_id': item['user_id'],
|
||||
'username': item['username'],
|
||||
'friendly_name': friendly_name,
|
||||
'user_thumb': user_thumb,
|
||||
'email': item['email'],
|
||||
'is_home_user': item['is_home_user'],
|
||||
'is_allow_sync': item['is_allow_sync'],
|
||||
'is_restricted': item['is_restricted'],
|
||||
'do_notify': item['do_notify'],
|
||||
'keep_history': item['keep_history']
|
||||
}
|
||||
return user_details
|
||||
else:
|
||||
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Requesting user list refresh.")
|
||||
# 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()
|
||||
try:
|
||||
if str(user_id).isdigit():
|
||||
query = 'SELECT user_id, username, friendly_name, thumb AS user_thumb, custom_avatar_url AS custom_thumb, ' \
|
||||
|
@ -312,8 +282,8 @@ class Users(object):
|
|||
logger.warn(u"PlexPy Users :: Unable to execute database query for get_details: %s." % e)
|
||||
result = []
|
||||
|
||||
user_details = {}
|
||||
if result:
|
||||
user_details = {}
|
||||
for item in result:
|
||||
if item['friendly_name']:
|
||||
friendly_name = item['friendly_name']
|
||||
|
@ -338,21 +308,28 @@ class Users(object):
|
|||
'do_notify': item['do_notify'],
|
||||
'keep_history': item['keep_history']
|
||||
}
|
||||
return user_details
|
||||
|
||||
user_details = get_user_details(user_id=user_id, user=user)
|
||||
|
||||
if user_details:
|
||||
return user_details
|
||||
|
||||
else:
|
||||
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Requesting user list refresh.")
|
||||
# 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()
|
||||
|
||||
user_details = get_user_details(user_id=user_id, user=user)
|
||||
|
||||
if user_details:
|
||||
return user_details
|
||||
|
||||
else:
|
||||
logger.warn(u"PlexPy Users :: Unable to retrieve user from local database. Returning 'Local' user.")
|
||||
# If there is no user data we must return something
|
||||
# Use "Local" user to retain compatibility with PlexWatch database value
|
||||
return {'user_id': None,
|
||||
'username': 'Local',
|
||||
'friendly_name': 'Local',
|
||||
'user_thumb': common.DEFAULT_USER_THUMB,
|
||||
'email': '',
|
||||
'is_home_user': 0,
|
||||
'is_allow_sync': 0,
|
||||
'is_restricted': 0,
|
||||
'do_notify': 0,
|
||||
'keep_history': 0
|
||||
}
|
||||
return default_return
|
||||
|
||||
def get_watch_time_stats(self, user_id=None):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
PLEXPY_VERSION = "master"
|
||||
PLEXPY_RELEASE_VERSION = "1.3.6"
|
||||
PLEXPY_RELEASE_VERSION = "1.3.7"
|
||||
|
|
|
@ -37,10 +37,13 @@ def start_thread():
|
|||
def run():
|
||||
from websocket import create_connection
|
||||
|
||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||
plexpy.CONFIG.PMS_IP,
|
||||
plexpy.CONFIG.PMS_PORT
|
||||
)
|
||||
if plexpy.CONFIG.PMS_SSL and plexpy.CONFIG.PMS_URL[:5] == 'https':
|
||||
uri = plexpy.CONFIG.PMS_URL.replace('https://', 'wss://') + '/:/websockets/notifications'
|
||||
else:
|
||||
uri = 'ws://%s:%s/:/websockets/notifications' % (
|
||||
plexpy.CONFIG.PMS_IP,
|
||||
plexpy.CONFIG.PMS_PORT
|
||||
)
|
||||
|
||||
# Set authentication token (if one is available)
|
||||
if plexpy.CONFIG.PMS_TOKEN:
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -15,12 +15,13 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
import cherrypy
|
||||
import plexpy
|
||||
|
||||
import cherrypy
|
||||
from plexpy import logger
|
||||
from plexpy.webserve import WebInterface
|
||||
import plexpy
|
||||
from plexpy.helpers import create_https_certificates
|
||||
from plexpy.webserve import WebInterface
|
||||
|
||||
|
||||
|
||||
def initialize(options):
|
||||
|
@ -31,17 +32,15 @@ def initialize(options):
|
|||
https_key = options['https_key']
|
||||
|
||||
if enable_https:
|
||||
# If either the HTTPS certificate or key do not exist, try to make
|
||||
# self-signed ones.
|
||||
if not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key)):
|
||||
# If either the HTTPS certificate or key do not exist, try to make self-signed ones.
|
||||
if plexpy.CONFIG.HTTPS_CREATE_CERT and \
|
||||
(not (https_cert and os.path.exists(https_cert)) or not (https_key and os.path.exists(https_key))):
|
||||
if not create_https_certificates(https_cert, https_key):
|
||||
logger.warn("Unable to create certificate and key. Disabling " \
|
||||
"HTTPS")
|
||||
logger.warn("Unable to create certificate and key. Disabling HTTPS")
|
||||
enable_https = False
|
||||
|
||||
if not (os.path.exists(https_cert) and os.path.exists(https_key)):
|
||||
logger.warn("Disabled HTTPS because of missing certificate and " \
|
||||
"key.")
|
||||
logger.warn("Disabled HTTPS because of missing certificate and key.")
|
||||
enable_https = False
|
||||
|
||||
options_dict = {
|
||||
|
@ -63,13 +62,17 @@ def initialize(options):
|
|||
protocol = "http"
|
||||
|
||||
logger.info("Starting PlexPy web server on %s://%s:%d/", protocol,
|
||||
options['http_host'], options['http_port'])
|
||||
options['http_host'], options['http_port'])
|
||||
cherrypy.config.update(options_dict)
|
||||
|
||||
conf = {
|
||||
'/': {
|
||||
'tools.staticdir.root': os.path.join(plexpy.PROG_DIR, 'data'),
|
||||
'tools.proxy.on': options['http_proxy'] # pay attention to X-Forwarded-Proto header
|
||||
'tools.proxy.on': options['http_proxy'], # pay attention to X-Forwarded-Proto header
|
||||
'tools.gzip.on': True,
|
||||
'tools.gzip.mime_types': ['text/html', 'text/plain', 'text/css',
|
||||
'text/javascript', 'application/json',
|
||||
'application/javascript']
|
||||
},
|
||||
'/interfaces': {
|
||||
'tools.staticdir.on': True,
|
||||
|
@ -87,15 +90,15 @@ def initialize(options):
|
|||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': "js"
|
||||
},
|
||||
'/favicon.ico': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': os.path.join(os.path.abspath(
|
||||
os.curdir), "images" + os.sep + "favicon.ico")
|
||||
},
|
||||
'/cache': {
|
||||
'tools.staticdir.on': True,
|
||||
'tools.staticdir.dir': plexpy.CONFIG.CACHE_DIR
|
||||
},
|
||||
'/favicon.ico': {
|
||||
'tools.staticfile.on': True,
|
||||
'tools.staticfile.filename': os.path.abspath(os.path.join(plexpy.PROG_DIR, 'data/interfaces/default/images/favicon.ico'))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if options['http_password']:
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue