Merge branch 'dev'

This commit is contained in:
Jonathan Wong 2015-11-24 18:51:35 -08:00
commit 893c91a15d
38 changed files with 1795 additions and 622 deletions

View file

@ -1,5 +1,30 @@
# Changelog # Changelog
## v1.2.4 (2015-11-24)
* Add filtering by media type in the history table
* Add IFTTT notification agent
* Add Telegram notification agent
* Add notifications for recently added media
* Add notifications for server down and remote access down
* Add more metadata to notifications options
* Add IP address to notification options (for PMS 0.9.14 and above)
* Add server uptime to notification options
* Add IP address to current activity
* Add IPv6 address logging
* Add PMS server name to the page title
* Fix bug in "Last Watched" statistic
* Fix bug in search query
* Fix bug on user pages for usernames with single quotes
* Fix name for new Plex Media Center
* Fix Pushover notifications with unicode characters
* Fix bug with showing old usernames in datatables
* Fix bug with "Please verify your server" in settings
* Change IP lookup provider
* Change notifications custom body text to larger text box
* Change movie/tv logging and notifications into individual options
## v1.2.3 (2015-10-18) ## v1.2.3 (2015-10-18)
* Added "remaining time" as notification substitution. * Added "remaining time" as notification substitution.

View file

@ -6,11 +6,7 @@ A python based web application for monitoring, analytics and notifications for P
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb). This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).
* plexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program * PlexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
If you'd like to buy me a beer, hit the donate button below. All donations go to the project maintainer (primarily for the procurement of liquid refreshment).
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9HZK9BDJLKT6)
###Support ###Support

View file

@ -7,7 +7,7 @@ from plexpy import version
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>PlexPy - ${title}</title> <title>PlexPy - ${title} | ${server_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content=""> <meta name="description" content="">
<meta name="author" content=""> <meta name="author" content="">
@ -61,7 +61,7 @@ from plexpy import version
<form action="search" method="post" class="form" id="search_form"> <form action="search" method="post" class="form" id="search_form">
<div class="input-group"> <div class="input-group">
<span class="input-textbox"> <span class="input-textbox">
<input type="text" class="form-control" name="search_query" id="search_query" aria-label="Search" placeholder="Search..."/> <input type="text" class="form-control" name="query" id="query" aria-label="Search" placeholder="Search..."/>
</span> </span>
<span class="input-group-btn"> <span class="input-group-btn">
<button class="btn btn-dark btn-inactive" type="submit" id="search_button"><i class="fa fa-search"></i></button> <button class="btn btn-dark btn-inactive" type="submit" id="search_button"><i class="fa fa-search"></i></button>
@ -131,19 +131,19 @@ ${next.headerIncludes()}
</script> </script>
<script> <script>
$('#search_form').submit(function (e) { $('#search_form').submit(function (e) {
if ($('#search_query').hasClass('active') && $('#search_query').val().trim() != '') { if ($('#query').hasClass('active') && $('#query').val().trim() != '') {
$.ajax({ $.ajax({
type: 'post', type: 'post',
url: 'search', url: 'search',
data: { 'query': $('#search_query').val() } data: { 'query': $('#query').val() }
}) })
} else { } else {
e.preventDefault(); e.preventDefault();
$('#search_button').removeClass('btn-inactive'); $('#search_button').removeClass('btn-inactive');
$('#search_query').clearQueue().val('').animate({ right: '0', width: '250px' }).addClass('active').focus(); $('#query').clearQueue().val('').animate({ right: '0', width: '250px' }).addClass('active').focus();
} }
}) })
$('#search_query').on('blur', function (e) { $('#query').on('blur', function (e) {
if ($(this).val().trim() == '') { if ($(this).val().trim() == '') {
$(this).delay(200).animate({ right: '-250px', width: '0' }, function () { $(this).delay(200).animate({ right: '-250px', width: '0' }, function () {
$('#search_button').addClass('btn-inactive'); $('#search_button').addClass('btn-inactive');

View file

@ -341,6 +341,24 @@ input[type="color"],
border-radius: 3px; border-radius: 3px;
transition: background-color .3s; transition: background-color .3s;
} }
textarea.form-control {
height: initial;
margin: 5px 0 5px 0;
color: #fff;
border: 0px solid #444;
background: #555;
padding: 6px 12px;
background-color: #555;
border-radius: 3px;
transition: background-color .3s;
resize: none;
}
textarea.form-control:focus {
outline: 0;
color: #555;
background-color: #fff;
transition: background-color .3s;
}
.pagination > li > a, .pagination > li > a,
.pagination > li > span { .pagination > li > span {
position: relative; position: relative;
@ -688,6 +706,14 @@ a:hover .dashboard-activity-poster {
white-space: nowrap; white-space: nowrap;
width: 150px; width: 150px;
} }
.dashboard-activity-poster-info-ip-address {
position: absolute;
bottom: 5px;
left: 10px;
text-align: left;
font-size: 12px;
color: #eee;
}
.dashboard-activity-poster-info-time { .dashboard-activity-poster-info-time {
position: absolute; position: absolute;
bottom: 5px; bottom: 5px;
@ -2472,7 +2498,7 @@ table[id^='history_child'] thead th {
display: inline-flex; display: inline-flex;
float: right; float: right;
} }
#search_form #search_query { #search_form #query {
width: 0; width: 0;
height: 34px; height: 34px;
margin-top: 0; margin-top: 0;
@ -2481,7 +2507,7 @@ table[id^='history_child'] thead th {
right: -250px; right: -250px;
border-radius: 3px 0 0 3px; border-radius: 3px 0 0 3px;
} }
#search_form #search_query.active { #search_form #query.active {
width: 250px; width: 250px;
right: 0px; right: 0px;
} }

View file

@ -32,6 +32,7 @@ user_thumb Returns the profile picture of the user owning the s
state Returns the state of the current session. Either 'playing', 'paused' or 'buffering'. state Returns the state of the current session. Either 'playing', 'paused' or 'buffering'.
title Returns the name of the episode, movie or music track. title Returns the name of the episode, movie or music track.
year Returns the year of the episode, movie, or clip. year Returns the year of the episode, movie, or clip.
ip_address Returns the ip address of the stream.
player Returns the name of the platform used to play the stream. player Returns the name of the platform used to play the stream.
platform Returns the type of platform used to play the stream. platform Returns the type of platform used to play the stream.
throttled Returns true if the transcode session is throttled. throttled Returns true if the transcode session is throttled.
@ -191,6 +192,13 @@ DOCUMENTATION :: END
</div> </div>
% if a['media_type'] != 'photo': % if a['media_type'] != 'photo':
<div class="dashboard-activity-poster-info-bar"> <div class="dashboard-activity-poster-info-bar">
<div class="dashboard-activity-poster-info-ip-address">
% if a['ip_address']:
<span>IP: ${a['ip_address']}</span>
% else:
<span>IP: N/A</span>
% endif
</div>
<div class="dashboard-activity-poster-info-time"> <div class="dashboard-activity-poster-info-time">
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span> <span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
</div> </div>

View file

@ -76,19 +76,50 @@
<script src="interfaces/default/js/moment-with-locale.js"></script> <script src="interfaces/default/js/moment-with-locale.js"></script>
<script src="interfaces/default/js/tables/history_table.js"></script> <script src="interfaces/default/js/tables/history_table.js"></script>
<script> <script>
$(document).ready(function() { $(document).ready(function () {
history_table_options.ajax = { function loadHistoryTable(media_type) {
"url": "get_history", history_table_options.ajax = {
type: "post", url: 'get_history',
data: function ( d ) { type: 'post',
return { 'json_data': JSON.stringify( d ) }; data: function (d) {
return {
'json_data': JSON.stringify(d),
'media_type': media_type
};
}
} }
} history_table = $('#history_table').DataTable(history_table_options);
history_table = $('#history_table').DataTable(history_table_options); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 11] }); $(colvis.button()).appendTo('div.colvis-button-bar');
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table); clearSearchButton('history_table', history_table);
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
<label class="btn btn-dark active"> \
<input type="radio" name="media_type-filter" id="history-all" value="all" autocomplete="off"> All \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-movies" value="movie" autocomplete="off"> Movies \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-tv_shows" value="episode" autocomplete="off"> TV Shows \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-music" value="track" autocomplete="off"> Music \
</label> \
</div>');
$('#media_type-selection').on('change', function () {
$('#media_type-selection > label').removeClass('active');
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
$(selected_filter).closest('label').addClass('active');
media_type = $(selected_filter).val();
history_table.draw();
});
}
var media_type = 'all';
loadHistoryTable(media_type);
$('#row-edit-mode').on('click', function() { $('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200); $('#row-edit-mode-alert').fadeIn(200);

Binary file not shown.

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -33,7 +33,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<div class="padded-header" id="library-statistics-header"> <div class="padded-header" id="library-statistics-header">
<h3>Library Statistics</h3> <h3>Library Statistics <small>${config['pms_name']}</small></h3>
</div> </div>
<div id="library-stats" class="library-platforms"> <div id="library-stats" class="library-platforms">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div> <div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
@ -98,27 +98,6 @@
}); });
} }
function getLibraryStatsHeader() {
$.ajax({
"url": "get_servers_info",
type: "post",
cache: false,
async: true,
data: { },
complete: function (xhr, status) {
server_info = $.parseJSON(xhr.responseText);
var server_name = 'Server name not found';
for (var i in server_info) {
if (server_info[i].machine_identifier == '${config['pms_identifier']}') {
server_name = server_info[i].name
break;
}
}
$('#library-statistics-header h3').append(' <small>' + server_name + '</small>')
}
});
}
function getLibraryStats() { function getLibraryStats() {
$.ajax({ $.ajax({
url: 'library_stats', url: 'library_stats',
@ -170,7 +149,6 @@
}); });
getHomeStats(); getHomeStats();
getLibraryStatsHeader();
getLibraryStats(); getLibraryStats();

View file

@ -11,7 +11,7 @@ data :: Usable parameters (if not applicable for media type, blank value will be
== Global keys == == Global keys ==
rating_key Returns the unique identifier for the media item. rating_key Returns the unique identifier for the media item.
type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'. media_type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'.
art Returns the location of the item's artwork art Returns the location of the item's artwork
title Returns the name of the movie, show, episode, artist, album, or track. title Returns the name of the movie, show, episode, artist, album, or track.
duration Returns the standard runtime of the media. duration Returns the standard runtime of the media.
@ -60,7 +60,7 @@ DOCUMENTATION :: END
% if data: % if data:
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
% if data['type'] != 'library': % if data['media_type'] != 'library':
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div> <div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
% endif % endif
<div class="summary-container"> <div class="summary-container">
@ -68,7 +68,7 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="summary-navbar-list"> <div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb"> <ul class="list-unstyled breadcrumb">
% if data['type'] == 'library': % if data['media_type'] == 'library':
% if data['library'] == 'movie': % if data['library'] == 'movie':
<li class="active">Movies</li> <li class="active">Movies</li>
% elif data['library'] == 'show': % elif data['library'] == 'show':
@ -76,29 +76,29 @@ DOCUMENTATION :: END
% elif data['library'] == 'artist': % elif data['library'] == 'artist':
<li class="active">Music</li> <li class="active">Music</li>
% endif % endif
% elif data['type'] == 'movie': % elif data['media_type'] == 'movie':
<li><a href="info?item_id=movie">Movies</a></li> <li><a href="info?item_id=movie">Movies</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['type'] == 'show': % elif data['media_type'] == 'show':
<li><a href="info?item_id=show">TV Shows</a></li> <li><a href="info?item_id=show">TV Shows</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['type'] == 'season': % elif data['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li> <li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Season ${data['index']}</li> <li class="active">Season ${data['index']}</li>
% elif data['type'] == 'episode': % elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li> <li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></li> <li><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></li>
<li class="active">Episode ${data['index']} - ${data['title']}</li> <li class="active">Episode ${data['index']} - ${data['title']}</li>
% elif data['type'] == 'artist': % elif data['media_type'] == 'artist':
<li><a href="info?item_id=artist">Music</a></li> <li><a href="info?item_id=artist">Music</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['type'] == 'album': % elif data['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li> <li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">${data['title']}</li> <li class="active">${data['title']}</li>
% elif data['type'] == 'track': % elif data['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li> <li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
@ -108,28 +108,28 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% if data['type'] != 'library': % if data['media_type'] != 'library':
<div class="summary-content-title-wrapper"> <div class="summary-content-title-wrapper">
<div class="col-md-9"> <div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm"> <div class="summary-content-poster hidden-xs hidden-sm">
% if data['type'] == 'track': % if data['media_type'] == 'track':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web"> <a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web">
% elif data['type'] != 'library': % elif data['media_type'] != 'library':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web"> <a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web">
% endif % endif
% if data['type'] == 'episode': % if data['media_type'] == 'episode':
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=poster);"> <div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=poster);">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
</div> </div>
</div> </div>
% elif data['type'] == 'artist' or data['type'] == 'album' or data['type'] == 'track': % elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=poster);"> <div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=poster);">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
</div> </div>
</div> </div>
% elif data['type'] != 'library': % elif data['media_type'] != 'library':
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"> <div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
@ -139,19 +139,19 @@ DOCUMENTATION :: END
</a> </a>
</div> </div>
<div class="summary-content-title"> <div class="summary-content-title">
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'artist': % if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'artist':
<h1>&nbsp;</h1><h1>${data['title']}</h1> <h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['type'] == 'season': % elif data['media_type'] == 'season':
<h1>&nbsp;</h1><h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1> <h1>&nbsp;</h1><h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
<h3 class="hidden-xs">S${data['index']}</h3> <h3 class="hidden-xs">S${data['index']}</h3>
% elif data['type'] == 'episode': % elif data['media_type'] == 'episode':
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1> <h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
<h2>${data['title']}</h2> <h2>${data['title']}</h2>
<h3 class="hidden-xs">S${data['parent_index']} &middot; E${data['index']}</h3> <h3 class="hidden-xs">S${data['parent_index']} &middot; E${data['index']}</h3>
% elif data['type'] == 'album': % elif data['media_type'] == 'album':
<h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1> <h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
<h2>${data['title']}</h2> <h2>${data['title']}</h2>
% elif data['type'] == 'track': % elif data['media_type'] == 'track':
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1> <h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
<h2><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2> <h2><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2>
<h3 class="hidden-xs">T${data['index']}</h3> <h3 class="hidden-xs">T${data['index']}</h3>
@ -161,13 +161,13 @@ DOCUMENTATION :: END
</div> </div>
% endif % endif
<div class="summary-content-wrapper"> <div class="summary-content-wrapper">
% if data['type'] != 'library': % if data['media_type'] != 'library':
<div class="col-md-9"> <div class="col-md-9">
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season': % if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'season':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div>
% elif data['type'] == 'episode': % elif data['media_type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 40px;"></div> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 40px;"></div>
% elif data['type'] == 'artist' or data['type'] == 'album' or data['type'] == 'track': % elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;"></div> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;"></div>
% else: % else:
<div class="summary-content-padding hidden-xs hidden-sm"></div> <div class="summary-content-padding hidden-xs hidden-sm"></div>
@ -189,13 +189,13 @@ DOCUMENTATION :: END
% endif % endif
</div> </div>
<div class="summary-content-details-tag"> <div class="summary-content-details-tag">
% if data['type'] == 'movie': % if data['media_type'] == 'movie':
Year <strong> ${data['year']}</strong> Year <strong> ${data['year']}</strong>
% elif data['type'] == 'show': % elif data['media_type'] == 'show':
Aired <strong> ${data['year']}</strong> Aired <strong> ${data['year']}</strong>
% elif data['type'] == 'episode': % elif data['media_type'] == 'episode':
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong> Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['type'] == 'album' or data['type'] == 'track': % elif data['media_type'] == 'album' or data['media_type'] == 'track':
Released <strong> ${data['year']}</strong> Released <strong> ${data['year']}</strong>
% endif % endif
</div> </div>
@ -268,7 +268,7 @@ DOCUMENTATION :: END
% endif % endif
</div> </div>
</div> </div>
% if data['type'] == 'show': % if data['media_type'] == 'show':
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
@ -279,7 +279,7 @@ DOCUMENTATION :: END
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div> <div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
</div> </div>
</div> </div>
% elif data['type'] == 'season': % elif data['media_type'] == 'season':
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
@ -290,7 +290,7 @@ DOCUMENTATION :: END
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div> <div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
</div> </div>
</div> </div>
% elif data['type'] == 'artist': % elif data['media_type'] == 'artist':
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
@ -301,7 +301,7 @@ DOCUMENTATION :: END
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div> <div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
</div> </div>
</div> </div>
% elif data['type'] == 'album': % elif data['media_type'] == 'album':
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
@ -382,49 +382,38 @@ DOCUMENTATION :: END
<div class="summary-navbar"> <div class="summary-navbar">
<div class="col-md-12"> <div class="col-md-12">
<div class="summary-navbar-list"> <div class="summary-navbar-list">
% if query: <ul class="list-unstyled breadcrumb">
% if query['media_type'] == 'movie': % if query:
<span>Movies</span> % if query['media_type'] == 'movie':
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> <li><a href="info?item_id=movie">Movies</a></li>
<span>${query['title']}</span> <li class="active">${query['title']}</li>
% elif query['media_type'] == 'show': % elif query['media_type'] == 'show':
<span>TV Shows</span> <li><a href="info?item_id=show">TV Shows</a></li>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> <li class="active">${query['grandparent_title']}</li>
<span>${query['grandparent_title']}</span> % elif query['media_type'] == 'season':
% elif query['media_type'] == 'season': <li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<span class="hidden-xs hidden-sm">TV Shows</span> <li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> <li class="active">Season ${query['parent_media_index']}</li>
<span class="hidden-xs hidden-sm">${query['grandparent_title']}</span> % elif query['media_type'] == 'episode':
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> <li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<span>Season ${query['parent_media_index']}</span> <li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
% elif query['media_type'] == 'episode': <li>Season ${query['parent_media_index']}</li>
<span class="hidden-xs hidden-sm">TV Shows</span> <li class="active">Episode ${query['media_index']} - ${query['title']}</li>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> % elif query['media_type'] == 'artist':
<span class="hidden-xs hidden-sm">${query['grandparent_title']}</span> <li><a href="info?item_id=artist">Music</a></li>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> <li class="active">${query['grandparent_title']}</li>
<span>Season ${query['parent_media_index']}</span> % elif query['media_type'] == 'album':
<span><i class="fa fa-chevron-right"></i></span> <li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<span>Episode ${query['media_index']} - ${query['title']}</span> <li>${query['grandparent_title']}</li>
% elif query['media_type'] == 'artist': <li class="active">${query['parent_title']}</li>
<span>Music</span> % elif query['media_type'] == 'track':
<span><i class="fa fa-chevron-right"></i></span> <li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<span>${query['grandparent_title']}</span> <li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
% elif query['media_type'] == 'album': <li>${query['parent_title']}</li>
<span class="hidden-xs hidden-sm">Music</span> <li class="active">Track ${query['media_index']} - ${query['title']}</li>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span> % endif
<span>${query['grandparent_title']}</span> % endif
<span><i class="fa fa-chevron-right"></i></span> </ul>
<span>${query['parent_title']}</span>
% elif query['media_type'] == 'track':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm">${query['grandparent_title']}</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>${query['parent_title']}</span>
<span><i class="fa fa-chevron-right"></i></span>
<span>Track ${query['media_index']} - ${query['title']}</span>
% endif
% endif
</div> </div>
</div> </div>
</div> </div>
@ -510,54 +499,54 @@ DOCUMENTATION :: END
% if data: % if data:
<script src="interfaces/default/js/tables/history_table.js"></script> <script src="interfaces/default/js/tables/history_table.js"></script>
% if data['type'] == 'library': % if data['media_type'] == 'library':
<script> <script>
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
"url": "get_history", url: 'get_history',
type: 'post', type: 'post',
data: function ( d ) { data: function ( d ) {
return { 'json_data': JSON.stringify( d ), return { 'json_data': JSON.stringify( d ),
'media_type': '${data['media_type']}' }; 'media_type': "${data['media_type_filter']}" };
} }
} }
} }
</script> </script>
% elif data['type'] == 'show' or data['type'] == 'artist': % elif data['media_type'] == 'show' or data['media_type'] == 'artist':
<script> <script>
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
"url": "get_history", url: 'get_history',
type: 'post', type: 'post',
data: function ( d ) { data: function ( d ) {
return { 'json_data': JSON.stringify( d ), return { 'json_data': JSON.stringify( d ),
'grandparent_rating_key': ${data['rating_key']} }; 'grandparent_rating_key': "${data['rating_key']}" };
} }
} }
} }
</script> </script>
% elif data['type'] == 'season' or data['type'] == 'album': % elif data['media_type'] == 'season' or data['media_type'] == 'album':
<script> <script>
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
"url": "get_history", url: 'get_history',
type: 'post', type: 'post',
data: function ( d ) { data: function ( d ) {
return { 'json_data': JSON.stringify( d ), return { 'json_data': JSON.stringify( d ),
'parent_rating_key': ${data['rating_key']} }; 'parent_rating_key': "${data['rating_key']}" };
} }
} }
} }
</script> </script>
% elif data['type'] == 'episode' or data['type'] == 'track' or data['type'] == 'movie': % elif data['media_type'] == 'episode' or data['media_type'] == 'track' or data['media_type'] == 'movie':
<script> <script>
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
"url": "get_history", url: 'get_history',
type: 'post', type: 'post',
data: function ( d ) { data: function ( d ) {
return { 'json_data': JSON.stringify( d ), return { 'json_data': JSON.stringify( d ),
'rating_key': ${data['rating_key']} }; 'rating_key': "${data['rating_key']}" };
} }
} }
} }
@ -610,11 +599,11 @@ DOCUMENTATION :: END
}); });
}); });
</script> </script>
% if data['type'] == 'show' or data['type'] == 'season' or data['type'] == 'artist' or data['type'] == 'album': % if data['media_type'] == 'show' or data['media_type'] == 'season' or data['media_type'] == 'artist' or data['media_type'] == 'album':
<script> <script>
$.ajax({ $.ajax({
url: 'get_item_children', url: 'get_item_children',
type: "GET", type: 'GET',
async: true, async: true,
data: { rating_key : ${data['rating_key']} }, data: { rating_key : ${data['rating_key']} },
complete: function(xhr, status) { complete: function(xhr, status) {
@ -622,7 +611,16 @@ DOCUMENTATION :: END
}); });
</script> </script>
% endif % endif
% if data['type'] != 'library' and data['rating']: % if data['media_type'] != 'library':
<!--
<script>
$('#row-edit-mode').after('<a href="info?item_id=${data['rating_key']}" class="btn btn-danger btn-edit" id="fix-metadata" \
data-toggle="tooltip" data-placement="left" title="Fix metadata if the item was moved in Plex"><i class="fa fa-wrench"></i> Fix Metadata</a>');
$('#fix-metadata').tooltip();
</script>
-->
% endif
% if data['media_type'] != 'library' and data['rating']:
<script> <script>
// Convert rating to 5 star rating type // Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2); var starRating = Math.round(${data['rating']} / 2);
@ -638,9 +636,9 @@ DOCUMENTATION :: END
<script> <script>
$.ajax({ $.ajax({
url: 'get_search_results_children', url: 'get_search_results_children',
type: "GET", type: 'GET',
async: true, async: true,
data: {'query': "${query['query_string']}", data: {'query': "${query['query_string'].replace('"','\\"')}",
'media_type': "${query['media_type']}", 'media_type': "${query['media_type']}",
'season_index': "${query['parent_media_index']}" 'season_index': "${query['parent_media_index']}"
}, },

View file

@ -17,8 +17,8 @@
<h4><strong>Location Details</strong></h4> <h4><strong>Location Details</strong></h4>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>Country: <strong><span id="country"></span></strong></li> <li>Country: <strong><span id="country"></span></strong></li>
<li>City: <strong><span id="city"></span></strong></li>
<li>Region: <strong><span id="region"></span></strong></li> <li>Region: <strong><span id="region"></span></strong></li>
<li>City: <strong><span id="city"></span></strong></li>
<li>Timezone: <strong><span id="timezone"></span></strong></li> <li>Timezone: <strong><span id="timezone"></span></strong></li>
<li>Latitude: <strong><span id="lat"></span></strong></li> <li>Latitude: <strong><span id="lat"></span></strong></li>
<li>Longitude: <strong><span id="lon"></span></strong></li> <li>Longitude: <strong><span id="lon"></span></strong></li>
@ -27,14 +27,12 @@
<div class="col-md-6"> <div class="col-md-6">
<h4><strong>Connection Details</strong></h4> <h4><strong>Connection Details</strong></h4>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>ISP: <strong><span id="isp"></span></strong></li> <li>Organization: <strong><span id="organization"></span></strong></li>
<li>Organization: <strong><span id="org"></span></strong></li>
<li>AS: <strong><span id="as"></span></strong></li>
</ul> </ul>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<span class="text-muted">Service provided by ip-api.com.</span> <span class="text-muted">Telize service written by <a href="https://github.com/fcambus/telize" target="_blank">Frederic Cambus</a>.</span>
</div> </div>
</div> </div>
</div> </div>
@ -43,7 +41,7 @@
<script> <script>
function getUserLocation(ip_address) { function getUserLocation(ip_address) {
$.ajax({ $.ajax({
url: 'http://ip-api.com/json/' + ip_address, url: 'https://telize.myhtpc.co.za/geoip/' + ip_address,
cache: true, cache: true,
async: true, async: true,
type: 'GET', type: 'GET',
@ -55,13 +53,11 @@
$('#modal_header_ip_address').html('<i class="fa fa-map-marker"></i> IP Address: ' + ip_address); $('#modal_header_ip_address').html('<i class="fa fa-map-marker"></i> IP Address: ' + ip_address);
$('#country').html(data.country); $('#country').html(data.country);
$('#city').html(data.city); $('#city').html(data.city);
$('#region').html(data.regionName); $('#region').html(data.region);
$('#timezone').html(data.timezone); $('#timezone').html(data.timezone);
$('#lat').html(data.lat); $('#lat').html(data.latitude);
$('#lon').html(data.lon); $('#lon').html(data.longitude);
$('#isp').html(data.isp); $('#organization').html(data.organization);
$('#org').html(data.org);
$('#as').html(data.as);
}, },
timeout: 5000 timeout: 5000
}); });

View file

@ -223,6 +223,8 @@ function getPlatformImagePath(platformName) {
return 'interfaces/default/images/platforms/win8.png'; return 'interfaces/default/images/platforms/win8.png';
} else if (platformName.indexOf("Windows phone") > -1) { } else if (platformName.indexOf("Windows phone") > -1) {
return 'interfaces/default/images/platforms/wp.png'; return 'interfaces/default/images/platforms/wp.png';
} else if (platformName.indexOf("Plex Media Player") > -1) {
return 'interfaces/default/images/platforms/pmp.png';
} else { } else {
return 'interfaces/default/images/platforms/default.png'; return 'interfaces/default/images/platforms/default.png';
} }
@ -230,7 +232,9 @@ function getPlatformImagePath(platformName) {
function isPrivateIP(ip_address) { function isPrivateIP(ip_address) {
if (ip_address.indexOf(".") > -1) { if (ip_address.indexOf(".") > -1) {
var parts = ip_address.split('.'); // get IPv4 mapped address (xxx.xxx.xxx.xxx) from IPv6 addresss (::ffff:xxx.xxx.xxx.xxx)
var parts = ip_address.split(":");
var parts = parts[parts.length - 1].split('.');
if (parts[0] === '10' || if (parts[0] === '10' ||
(parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) || (parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) ||
(parts[0] === '192' && parts[1] === '168')) { (parts[0] === '192' && parts[1] === '168')) {

View file

@ -121,6 +121,12 @@ from plexpy import helpers
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut(); $('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
}); });
$('#testIFTTT').click(function () {
$.get("/test_ifttt",
function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
// Never send checkbox values directly, always substitute value in hidden input. // Never send checkbox values directly, always substitute value in hidden input.
$('.checkboxes').click(function() { $('.checkboxes').click(function() {
var configToggle = $(this).data('id'); var configToggle = $(this).data('id');

View file

@ -57,6 +57,27 @@ from plexpy import helpers
</label> </label>
<p class="help-block">Trigger notification when a media item triggers the defined buffer threshold.</p> <p class="help-block">Trigger notification when a media item triggers the defined buffer threshold.</p>
</div> </div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_created" ${helpers.checked(data['on_created'])} class="toggle-switches">
Notify on recently added
</label>
<p class="help-block">Trigger notification when a media item is added to the Plex Media Server.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extdown" ${helpers.checked(data['on_extdown'])} class="toggle-switches">
Notify on Plex remote access down
</label>
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached externally.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intdown" ${helpers.checked(data['on_intdown'])} class="toggle-switches">
Notify on Plex server down
</label>
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached internally.</p>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -81,6 +102,7 @@ from plexpy import helpers
console.log('success'); console.log('success');
} }
}); });
$('.toggle-notification-triggers-modal[data-id=' + configToggle + ']').addClass('active');
} else { } else {
var data = {}; var data = {};
data[$(this).data('config-name')] = 0; data[$(this).data('config-name')] = 0;
@ -92,6 +114,9 @@ from plexpy import helpers
console.log('success'); console.log('success');
} }
}); });
if (!($('.toggle-switches').is(":checked"))) {
$('.toggle-notification-triggers-modal[data-id=' + configToggle + ']').removeClass('active');
}
} }
}); });
</script> </script>

View file

@ -11,13 +11,13 @@ data[array_index] :: Usable parameters
== Global keys == == Global keys ==
rating_key Returns the unique identifier for the media item. rating_key Returns the unique identifier for the media item.
type Returns the type of media. Either 'movie' or 'season'. media_type Returns the media type of media. Either 'movie' or 'season' or 'album'.
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy. thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
added_at Returns the time when the media was added to the library. added_at Returns the time when the media was added to the library.
title Returns the name of the movie or season. title Returns the name of the movie or season.
parent_title Returns the name of the TV Show a season belongs too. parent_title Returns the name of the TV Show a season belongs too.
== Only if 'type' is 'movie' == == Only if 'media_type' is 'movie' ==
year Returns the movie release year. year Returns the movie release year.
DOCUMENTATION :: END DOCUMENTATION :: END
@ -29,7 +29,7 @@ DOCUMENTATION :: END
% for item in data: % for item in data:
<div class="dashboard-recent-media-instance"> <div class="dashboard-recent-media-instance">
<li> <li>
% if item['type'] == 'season' or item['type'] == 'movie': % if item['media_type'] == 'season' or item['media_type'] == 'movie':
<a href="info?item_id=${item['rating_key']}"> <a href="info?item_id=${item['rating_key']}">
<div class="dashboard-recent-media-poster"> <div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"> <div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
@ -43,16 +43,16 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season': % if item['media_type'] == 'season':
<h3 title="${item['parent_title']}">${item['parent_title']}</h3> <h3 title="${item['parent_title']}">${item['parent_title']}</h3>
<h3 class="text-muted">${item['title']}</h3> <h3 class="text-muted">${item['title']}</h3>
% elif item['type'] == 'movie': % elif item['media_type'] == 'movie':
<h3 title="${item['title']}">${item['title']}</h3> <h3 title="${item['title']}">${item['title']}</h3>
<h3 class="text-muted">${item['year']}</h3> <h3 class="text-muted">${item['year']}</h3>
% endif % endif
</div> </div>
</a> </a>
% elif item['type'] == 'album': % elif item['media_type'] == 'album':
<a href="info?item_id=${item['rating_key']}"> <a href="info?item_id=${item['rating_key']}">
<div class="dashboard-recent-media-cover"> <div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"> <div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">

View file

@ -26,13 +26,13 @@
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
<script> <script>
$('#search_button').removeClass('btn-inactive'); $('#search_button').removeClass('btn-inactive');
$('#search_query').val('${query}').css({ right: '0', width: '250px' }).addClass('active'); $('#query').val("${query.replace('"','\\"')}").css({ right: '0', width: '250px' }).addClass('active');
$.ajax({ $.ajax({
url: 'get_search_results_children', url: 'get_search_results_children',
type: "GET", type: "GET",
async: true, async: true,
data: {'query': "${query}"}, data: {'query': "${query.replace('"','\\"')}"},
complete: function (xhr, status) { complete: function (xhr, status) {
$("#search-results-list").html(xhr.responseText); $("#search-results-list").html(xhr.responseText);
} }

View file

@ -3,7 +3,7 @@
import plexpy import plexpy
from plexpy import notifiers, common, versioncheck from plexpy import notifiers, common, versioncheck
available_notification_agents = notifiers.available_notification_agents() available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['name'])
%> %>
<%def name="headIncludes()"> <%def name="headIncludes()">
</%def> </%def>
@ -273,9 +273,10 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="pms_ip">Plex IP or Hostname</label> <label for="pms_ip">Plex IP or Hostname</label>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<input type="text" class="pms-settings form-control" id="pms_ip" name="pms_ip" value="${config['pms_ip']}" size="30" data-parsley-trigger="change" aria-describedby="server-verified" required> <input type="text" class="pms-settings form-control" id="pms_ip" name="pms_ip" value="${config['pms_ip']}" size="30" data-parsley-trigger="change" aria-describedby="server-verified" data-parsley-errors-container="#pms_ip_error" required>
<span class="form-control-feedback" id="pms-verify" aria-hidden="true" style="display: none;"></span> <span class="form-control-feedback" id="pms-verify" aria-hidden="true" style="display: none;"></span>
</div> </div>
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
</div> </div>
<p class="help-block">IP Address or hostname for Plex Media Server.</p> <p class="help-block">IP Address or hostname for Plex Media Server.</p>
</div> </div>
@ -403,21 +404,33 @@ available_notification_agents = notifiers.available_notification_agents()
</label> </label>
<p class="help-block">Instead of polling the server at regular intervals let the server tell us when something happens. This is currently experimental. Encrypted websocket is not currently supported.</p> <p class="help-block">Instead of polling the server at regular intervals let the server tell us when something happens. This is currently experimental. Encrypted websocket is not currently supported.</p>
</div> </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>
</div>
<div class="padded-header"> <div class="padded-header">
<h3>History Logging</h3> <h3>History Logging</h3>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="video_logging_enable" name="video_logging_enable" value="1" ${config['video_logging_enable']}> Log Movies and TV <input type="checkbox" id="movie_logging_enable" name="movie_logging_enable" value="1" ${config['movie_logging_enable']}> Log Movies
</label> </label>
<p class="help-block">Keep records of all video items played from your Plex Media Server.</p> <p class="help-block">Keep records of all movie items played from your Plex Media Server.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="tv_logging_enable" name="tv_logging_enable" value="1" ${config['tv_logging_enable']}> Log TV Shows
</label>
<p class="help-block">Keep records of all TV show items played from your Plex Media Server.</p>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Log Music <input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Log Music
</label> </label>
<p class="help-block">Keep records of all audio items played from your Plex Media Server. VERY experimental.</p> <p class="help-block">Keep records of all music items played from your Plex Media Server.</p>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="logging_ignore_interval">Ignore Interval</label> <label for="logging_ignore_interval">Ignore Interval</label>
@ -435,7 +448,7 @@ available_notification_agents = notifiers.available_notification_agents()
</label> </label>
<span id="debugLogCheck" style="color: #eb8600; padding-left: 10px;"></span> <span id="debugLogCheck" style="color: #eb8600; padding-left: 10px;"></span>
<p class="help-block"> <p class="help-block">
Enable this to attempt to log the IP address of the user. 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).
</p> </p>
</div> </div>
@ -473,7 +486,12 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable Movie and TV Notifications <input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable Movie Notifications
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="tv_notify_enable" id="tv_notify_enable" value="1" ${config['tv_notify_enable']}> Enable TV Show Notifications
</label> </label>
</div> </div>
<div class="checkbox"> <div class="checkbox">
@ -483,7 +501,7 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="padded-header"> <div class="padded-header">
<h3>Notification Tuning</h3> <h3>Current Activity Notifications</h3>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -503,6 +521,27 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">Disable to prevent consecutive notifications (i.e. both watched &amp; stopped notifications).</p> <p class="help-block">Disable to prevent consecutive notifications (i.e. both watched &amp; stopped notifications).</p>
</div> </div>
<div class="padded-header">
<h3>Recently Added Notifications</h3>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="notify_recently_added_grandparent" id="notify_recently_added_grandparent" value="1" ${config['notify_recently_added_grandparent']}> Group notifications for recently added TV Shows or Music
</label>
<p class="help-block">Enable to only get one notification for recently added Episodes or Tracks. Movies are unaffected.</p>
</div>
<div class="form-group">
<label for="notify_recently_added_delay">Notification Delay</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_recently_added_delay" name="notify_recently_added_delay" value="${config['notify_recently_added_delay']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_recently_added_delay_error" required>
</div>
<div id="notify_recently_added_delay_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Set the delay for recently added notifications to allow metadata to be processed. Minimum 60 seconds.</p>
</div>
<div class="padded-header"> <div class="padded-header">
<h3>Custom Notification Messages</h3> <h3>Custom Notification Messages</h3>
</div> </div>
@ -515,9 +554,9 @@ available_notification_agents = notifiers.available_notification_agents()
<a href="#notify-text-tags-modal" data-toggle="modal">here</a> to view usage information. <a href="#notify-text-tags-modal" data-toggle="modal">here</a> to view usage information.
</p> </p>
<br/> <br/>
<ul id="accordion" class="accordion list-unstyled"> <ul id="accordion-session" class="accordion list-unstyled">
<li> <li>
<div class="link"><i class="fa fa-play"></i>Playback Start<i class="fa fa-chevron-down"></i></div> <div class="link"><i class="fa fa-play fa-fw"></i>&nbsp;Playback Start<i class="fa fa-chevron-down"></i></div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@ -527,14 +566,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notify_on_start_body_text">Message Body</label> <label for="notify_on_start_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_start_body_text" name="notify_on_start_body_text" value="${config['notify_on_start_body_text']}" data-parsley-trigger="change" required> <textarea class="form-control" id="notify_on_start_body_text" name="notify_on_start_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_start_body_text']}</textarea>
<p class="help-block">Set a custom body.</p> <p class="help-block">Set a custom body.</p>
</div> </div>
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
<div class="link"><i class="fa fa-stop"></i>Playback Stop<i class="fa fa-chevron-down"></i></div> <div class="link"><i class="fa fa-stop fa-fw"></i>&nbsp;Playback Stop<i class="fa fa-chevron-down"></i></div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@ -544,14 +583,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notify_on_stop_body_text">Message Body</label> <label for="notify_on_stop_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_stop_body_text" name="notify_on_stop_body_text" value="${config['notify_on_stop_body_text']}" data-parsley-trigger="change" required> <textarea class="form-control" id="notify_on_stop_body_text" name="notify_on_stop_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_stop_body_text']}</textarea>
<p class="help-block">Set a custom body.</p> <p class="help-block">Set a custom body.</p>
</div> </div>
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
<div class="link"><i class="fa fa-pause"></i>Playback Pause<i class="fa fa-chevron-down"></i></div> <div class="link"><i class="fa fa-pause fa-fw"></i>&nbsp;Playback Pause<i class="fa fa-chevron-down"></i></div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@ -561,14 +600,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notify_on_pause_body_text">Message Body</label> <label for="notify_on_pause_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_pause_body_text" name="notify_on_pause_body_text" value="${config['notify_on_pause_body_text']}" data-parsley-trigger="change" required> <textarea class="form-control" id="notify_on_pause_body_text" name="notify_on_pause_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_pause_body_text']}</textarea>
<p class="help-block">Set a custom body.</p> <p class="help-block">Set a custom body.</p>
</div> </div>
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
<div class="link"><i class="fa fa-play"></i>Playback Resume<i class="fa fa-chevron-down"></i></div> <div class="link"><i class="fa fa-play fa-fw"></i>&nbsp;Playback Resume<i class="fa fa-chevron-down"></i></div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@ -578,14 +617,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notify_on_resume_body_text">Message Body</label> <label for="notify_on_resume_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_resume_body_text" name="notify_on_resume_body_text" value="${config['notify_on_resume_body_text']}" data-parsley-trigger="change" required> <textarea class="form-control" id="notify_on_resume_body_text" name="notify_on_resume_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_resume_body_text']}</textarea>
<p class="help-block">Set a custom body.</p> <p class="help-block">Set a custom body.</p>
</div> </div>
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
<div class="link"><i class="fa fa-eye"></i>Watched<i class="fa fa-chevron-down"></i></div> <div class="link"><i class="fa fa-eye fa-fw"></i>&nbsp;Watched<i class="fa fa-chevron-down"></i></div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@ -595,14 +634,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notify_on_watched_body_text">Message Body</label> <label for="notify_on_watched_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_watched_body_text" name="notify_on_watched_body_text" value="${config['notify_on_watched_body_text']}" data-parsley-trigger="change" required> <textarea class="form-control" id="notify_on_watched_body_text" name="notify_on_watched_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_watched_body_text']}</textarea>
<p class="help-block">Set a custom body.</p> <p class="help-block">Set a custom body.</p>
</div> </div>
</li> </li>
</ul> </ul>
</li> </li>
<li> <li>
<div class="link"><i class="fa fa-spinner"></i>Buffer Warnings<i class="fa fa-chevron-down"></i></div> <div class="link"><i class="fa fa-spinner fa-fw"></i>&nbsp;Buffer Warnings<i class="fa fa-chevron-down"></i></div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group"> <div class="form-group">
@ -612,7 +651,60 @@ available_notification_agents = notifiers.available_notification_agents()
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="notify_on_buffer_body_text">Message Body</label> <label for="notify_on_buffer_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_buffer_body_text" name="notify_on_buffer_body_text" value="${config['notify_on_buffer_body_text']}" data-parsley-trigger="change" required> <textarea class="form-control" id="notify_on_buffer_body_text" name="notify_on_buffer_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_buffer_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
</ul>
<ul id="accordion-timeline" class="accordion list-unstyled">
<li>
<div class="link"><i class="fa fa-download fa-fw"></i>&nbsp;Recently Added<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_created_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_created_subject_text" name="notify_on_created_subject_text" value="${config['notify_on_created_subject_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom subject line.</p>
</div>
<div class="form-group">
<label for="notify_on_created_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_created_body_text" name="notify_on_created_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_created_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-server fa-fw"></i>&nbsp;Plex Remote Access Down<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_extdown_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_extdown_subject_text" name="notify_on_extdown_subject_text" value="${config['notify_on_extdown_subject_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom subject line.</p>
</div>
<div class="form-group">
<label for="notify_on_extdown_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_extdown_body_text" name="notify_on_extdown_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extdown_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-server fa-fw"></i>&nbsp;Plex Server Down<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_intdown_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_intdown_subject_text" name="notify_on_intdown_subject_text" value="${config['notify_on_intdown_subject_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom subject line.</p>
</div>
<div class="form-group">
<label for="notify_on_intdown_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_intdown_body_text" name="notify_on_intdown_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_intdown_body_text']}</textarea>
<p class="help-block">Set a custom body.</p> <p class="help-block">Set a custom body.</p>
</div> </div>
</li> </li>
@ -635,7 +727,7 @@ available_notification_agents = notifiers.available_notification_agents()
% for agent in available_notification_agents: % for agent in available_notification_agents:
<li> <li>
<span> <span>
% if agent['on_play'] or agent['on_stop'] or agent['on_pause'] or agent['on_resume'] or agent['on_buffer'] or agent['on_watched']: % if agent['on_play'] or agent['on_stop'] or agent['on_pause'] or agent['on_resume'] or agent['on_buffer'] or agent['on_watched'] or agent['on_created'] or agent['on_extdown'] or agent['on_intdown']:
<a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left active" data-toggle="modal"><i class="fa fa-lg fa-bell"></i></a> <a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left active" data-toggle="modal"><i class="fa fa-lg fa-bell"></i></a>
% else: % else:
<a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left" data-toggle="modal"><i class="fa fa-lg fa-bell"></i></a> <a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left" data-toggle="modal"><i class="fa fa-lg fa-bell"></i></a>
@ -845,6 +937,10 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{server_name}</strong></td> <td width="150"><strong>{server_name}</strong></td>
<td>The name of your Plex Server.</td> <td>The name of your Plex Server.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{server_uptime}</strong></td>
<td>The uptime (in days, hours, mins, secs) of your Plex Server.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{user}</strong></td> <td width="150"><strong>{user}</strong></td>
<td>The username of the person streaming.</td> <td>The username of the person streaming.</td>
@ -857,13 +953,17 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{player}</strong></td> <td width="150"><strong>{player}</strong></td>
<td>The name of the device being used for playback.</td> <td>The name of the device being used for playback.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{ip_address}</strong></td>
<td>The IP address of the device being used for playback. (PMS 0.9.14 and above)</td>
</tr>
<tr> <tr>
<td width="150"><strong>{media_type}</strong></td> <td width="150"><strong>{media_type}</strong></td>
<td>The type of media being played (movie, episode, track).</td> <td>The type of media being played (movie, episode, track).</td>
</tr> </tr>
<tr> <tr>
<td width="150"><strong>{title}</strong></td> <td width="150"><strong>{title}</strong></td>
<td>The title of the item being played.</td> <td>The full title of the item being played.</td>
</tr> </tr>
<tr> <tr>
<td width="150"><strong>{show_name}</strong></td> <td width="150"><strong>{show_name}</strong></td>
@ -881,6 +981,10 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{album_name}</strong></td> <td width="150"><strong>{album_name}</strong></td>
<td>The title of the album being played.</td> <td>The title of the album being played.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{track_name}</strong></td>
<td>The title of the track being played.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{season_num}</strong></td> <td width="150"><strong>{season_num}</strong></td>
<td>The season number for the media item if item is episode.</td> <td>The season number for the media item if item is episode.</td>
@ -913,10 +1017,30 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{content_rating}</strong></td> <td width="150"><strong>{content_rating}</strong></td>
<td>The content rating for the media item. (e.g. TV-MA, TV-PG, etc.)</td> <td>The content rating for the media item. (e.g. TV-MA, TV-PG, etc.)</td>
</tr> </tr>
<tr>
<td width="150"><strong>{directors}</strong></td>
<td>A list of directors for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{writers}</strong></td>
<td>A list of writers for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{actors}</strong></td>
<td>A list of actors for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{genres}</strong></td>
<td>A list of genres for the media item.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{summary}</strong></td> <td width="150"><strong>{summary}</strong></td>
<td>A short plot summary for the media item.</td> <td>A short plot summary for the media item.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{tagline}</strong></td>
<td>A tagline for the media item.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{rating}</strong></td> <td width="150"><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</td> <td>The rating (out of 10) for the item.</td>
@ -1051,17 +1175,21 @@ $(document).ready(function() {
} }
var configForm = $("#configUpdate"); var configForm = $("#configUpdate");
function saveSettings() {
if (configForm.parsley().validate()) {
doAjaxCall('configUpdate', $(this), 'tabs', true);
postSaveChecks();
return false;
} else {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspPlease verify your settings.', false, true, 2000, true)
}
}
$('.save-button').click(function() { $('.save-button').click(function() {
if ($("#pms_identifier").val() == "") { if ($("#pms_identifier").val() == "") {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspPlease verify your server.',false,true,2000,true) verifyServer(function () { saveSettings() });
} else { } else {
if (configForm.parsley().validate()) { saveSettings();
doAjaxCall('configUpdate',$(this),'tabs',true);
postSaveChecks();
return false;
} else {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspPlease verify your settings.',false,true,2000,true)
}
} }
}); });
@ -1147,7 +1275,7 @@ $(document).ready(function() {
verifyServer(); verifyServer();
}); });
function verifyServer() { function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val() var pms_ip = $("#pms_ip").val()
var pms_port = $("#pms_port").val() var pms_port = $("#pms_port").val()
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) { if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
@ -1170,10 +1298,15 @@ $(document).ready(function() {
$("#pms-verify").html('<i class="fa fa-check"></i>'); $("#pms-verify").html('<i class="fa fa-check"></i>');
$('#pms-verify').fadeIn('fast'); $('#pms-verify').fadeIn('fast');
$("#pms-ip-group").removeClass("has-error"); $("#pms-ip-group").removeClass("has-error");
if (_callback) {
_callback();
}
} else { } else {
$("#pms-verify").html('<i class="fa fa-close"></i>'); $("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast'); $('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error"); $("#pms-ip-group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspCould not verify your server.', false, true, 2000, true)
} }
} }
}); });
@ -1181,6 +1314,7 @@ $(document).ready(function() {
$("#pms-verify").html('<i class="fa fa-close"></i>'); $("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast'); $('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error"); $("#pms-ip-group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspCould not verify your server.', false, true, 2000, true)
} }
} }
@ -1199,7 +1333,7 @@ $(document).ready(function() {
'X-Plex-Version': '${common.VERSION_NUMBER}', 'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}', 'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}', 'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}', 'X-Plex-Client-Identifier': '${config["pms_uuid"]}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val()) 'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
}, },
error: function(jqXHR, textStatus, errorThrown) { error: function(jqXHR, textStatus, errorThrown) {
@ -1292,7 +1426,8 @@ $(document).ready(function() {
} }
} }
var accordion = new Accordion($('#accordion'), false); var accordion_session = new Accordion($('#accordion-session'), false);
var accordion_timeline = new Accordion($('#accordion-timeline'), false);
var cards = "${config['home_stats_cards']}".split(/[\s,]+/); var cards = "${config['home_stats_cards']}".split(/[\s,]+/);
cards.forEach(function (item) { cards.forEach(function (item) {
@ -1334,6 +1469,14 @@ $(document).ready(function() {
e.preventDefault() e.preventDefault()
}); });
// auto resizing textarea for custom notification message body
$('textarea[data-autoresize]').each(function() {
var offset = this.offsetHeight - this.clientHeight;
var resizeTextarea = function(el) {
$(el).css('height', 'auto').css('height', el.scrollHeight + offset);
};
$(this).on('focus keyup input', function() { resizeTextarea(this); }).removeAttr('data-autoresize');
});
}); });
</script> </script>
</%def> </%def>

View file

@ -262,33 +262,6 @@ from plexpy import helpers
<script src="interfaces/default/js/tables/user_ips.js"></script> <script src="interfaces/default/js/tables/user_ips.js"></script>
<script src="interfaces/default/js/tables/sync_table.js"></script> <script src="interfaces/default/js/tables/sync_table.js"></script>
<script> <script>
function recentlyWatched() {
var widthVal = $('body').find("#user-recently-watched").width();
var tmp = (widthVal-32) / 180;
if (tmp > 0) {
containerSize = parseInt(tmp);
} else {
containerSize = 1;
}
% if data['user_id']:
var user_id = ${data['user_id']};
% else:
var user_id = null;
% endif
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: { user_id: user_id, user: '${data['username']}', limit: containerSize },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
}
$(document).ready(function () { $(document).ready(function () {
% if data['user_id']: % if data['user_id']:
@ -297,13 +270,15 @@ from plexpy import helpers
var user_id = null; var user_id = null;
% endif % endif
var username = '${data['username'].replace("'", "\\'")}';
$("#edit-user-tooltip").tooltip(); $("#edit-user-tooltip").tooltip();
// Populate watch time stats // Populate watch time stats
$.ajax({ $.ajax({
url: 'get_user_watch_time_stats', url: 'get_user_watch_time_stats',
async: true, async: true,
data: { user_id: user_id, user: '${data['username']}' }, data: { user_id: user_id, user: username },
complete: function(xhr, status) { complete: function(xhr, status) {
$("#user-time-stats").html(xhr.responseText); $("#user-time-stats").html(xhr.responseText);
} }
@ -313,21 +288,23 @@ from plexpy import helpers
$.ajax({ $.ajax({
url: 'get_user_player_stats', url: 'get_user_player_stats',
async: true, async: true,
data: { user_id: user_id, user: '${data['username']}' }, data: { user_id: user_id, user: username },
complete: function(xhr, status) { complete: function(xhr, status) {
$("#user-player-stats").html(xhr.responseText); $("#user-player-stats").html(xhr.responseText);
} }
}); });
$( "#history-tab-btn" ).one( "click", function() { function loadHistoryTable(media_type) {
// Build watch history table // Build watch history table
history_table_options.ajax = { history_table_options.ajax = {
"url": "get_history", url: 'get_history',
type: 'post', type: 'post',
data: function ( d ) { data: function ( d ) {
return { 'json_data': JSON.stringify( d ), return {
'user_id': user_id, 'json_data': JSON.stringify( d ),
'user': "${data['username']}" 'user_id': user_id,
'user': username,
'media_type': media_type
}; };
} }
} }
@ -338,6 +315,34 @@ from plexpy import helpers
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table); clearSearchButton('history_table', history_table);
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
<label class="btn btn-dark active"> \
<input type="radio" name="media_type-filter" id="history-all" value="all" autocomplete="off"> All \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-movies" value="movie" autocomplete="off"> Movies \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-tv_shows" value="episode" autocomplete="off"> TV Shows \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-music" value="track" autocomplete="off"> Music \
</label> \
</div>');
$('#media_type-selection').on('change', function () {
$('#media_type-selection > label').removeClass('active');
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
$(selected_filter).closest('label').addClass('active');
media_type = $(selected_filter).val();
history_table.draw();
});
}
$( "#history-tab-btn" ).one( "click", function() {
var media_type = 'all';
loadHistoryTable(media_type);
}); });
$( "#ip-tab-btn" ).one( "click", function() { $( "#ip-tab-btn" ).one( "click", function() {
@ -348,7 +353,7 @@ from plexpy import helpers
data: function ( d ) { data: function ( d ) {
return { 'json_data': JSON.stringify( d ), return { 'json_data': JSON.stringify( d ),
'user_id': user_id, 'user_id': user_id,
'user': "${data['username']}" 'user': username
}; };
} }
} }
@ -363,7 +368,7 @@ from plexpy import helpers
"url": "get_sync", "url": "get_sync",
"data": function(d) { "data": function(d) {
d.user_id = user_id; d.user_id = user_id;
d.user = "${data['username']}"; d.user = username;
} }
} }
sync_table = $('#sync_table').DataTable(sync_table_options); sync_table = $('#sync_table').DataTable(sync_table_options);
@ -380,7 +385,7 @@ from plexpy import helpers
$("#edit-user-tooltip").tooltip('hide'); $("#edit-user-tooltip").tooltip('hide');
$.ajax({ $.ajax({
url: 'edit_user_dialog', url: 'edit_user_dialog',
data: { user_id: user_id, user: '${data['username']}' }, data: { user_id: user_id, user: username },
cache: false, cache: false,
async: true, async: true,
complete: function(xhr, status) { complete: function(xhr, status) {
@ -425,6 +430,33 @@ from plexpy import helpers
}); });
} }
}); });
function recentlyWatched() {
var widthVal = $('body').find("#user-recently-watched").width();
var tmp = (widthVal-32) / 180;
if (tmp > 0) {
containerSize = parseInt(tmp);
} else {
containerSize = 1;
}
% if data['user_id']:
var user_id = ${data['user_id']};
% else:
var user_id = null;
% endif
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: { user_id: user_id, user: username, limit: containerSize },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
}
recentlyWatched(); recentlyWatched();
$(window).resize(function() { $(window).resize(function() {

View file

@ -1,4 +1,4 @@
<% <%
import plexpy import plexpy
from plexpy import common from plexpy import common
%> %>
@ -104,9 +104,15 @@ from plexpy import common
<div class="wizard-card" data-cardname="card4"> <div class="wizard-card" data-cardname="card4">
<h3>Monitoring</h3> <h3>Monitoring</h3>
<p class="help-block">Keep records of all movie, TV show, or music items played from your Plex Media Server.</p>
<div class="wizard-input-section"> <div class="wizard-input-section">
<input type="checkbox" id="video_logging_enable" name="video_logging_enable" value="1" ${config['video_logging_enable']}> Log Movies and TV <input type="checkbox" id="movie_logging_enable" name="movie_logging_enable" value="1" ${config['movie_logging_enable']}> Log Movies
<p class="help-block">Keep records of all video items played from your Plex Media Server.</p> </div>
<div class="wizard-input-section">
<input type="checkbox" id="tv_logging_enable" name="tv_logging_enable" value="1" ${config['tv_logging_enable']}> Log TV Shows
</div>
<div class="wizard-input-section">
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Log Music
</div> </div>
<div class="wizard-input-section"> <div class="wizard-input-section">
<label for="logging_ignore_interval">Ignore Interval</label> <label for="logging_ignore_interval">Ignore Interval</label>
@ -118,19 +124,16 @@ from plexpy import common
</div> </div>
<p class="help-block">The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.</p> <p class="help-block">The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.</p>
</div> </div>
<!-- Music logging is still very experimental -- leave this for now.
<div class="wizard-input-section">
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1"> Log Music
<p class="help-block">Keep records of all audio items played from your Plex Media Server. VERY experimental.</p>
</div>
-->
</div> </div>
<div class="wizard-card" data-cardname="card5" data-validate="validateNotifications"> <div class="wizard-card" data-cardname="card5" data-validate="validateNotifications">
<h3>Notifications</h3> <h3>Notifications</h3>
<p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent conifgure this in <strong>Settings -> Notification Agents</strong> <p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent conifgure this in <strong>Settings -> Notification Agents</strong>
after you have completed this setup wizard.</p><br/> after you have completed this setup wizard.</p><br/>
<div class="wizard-input-section"> <div class="wizard-input-section">
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie and TV playback <input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie playback
</div>
<div class="wizard-input-section">
<input type="checkbox" name="tv_notify_enable" id="tv_notify_enable" value="1" ${config['tv_notify_enable']}> Enable notifications on TV Show playback
</div> </div>
<div class="wizard-input-section"> <div class="wizard-input-section">
<input type="checkbox" name="music_notify_enable" id="music_notify_enable" value="1" ${config['music_notify_enable']}> Enable notifications on Music playback <input type="checkbox" name="music_notify_enable" id="music_notify_enable" value="1" ${config['music_notify_enable']}> Enable notifications on Music playback
@ -262,6 +265,7 @@ from plexpy import common
if (ci != "undefined") { if (ci != "undefined") {
// To allow next step in the guide. // To allow next step in the guide.
// servers with clientIdentifier is verified // servers with clientIdentifier is verified
$("#pms_identifier").val(ci);
$("#pms_valid").val("valid"); $("#pms_valid").val("valid");
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show(); $("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
} else { } else {

View file

@ -31,7 +31,7 @@ except ImportError:
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.interval import IntervalTrigger
from plexpy import versioncheck, logger, activity_pinger, plextv from plexpy import versioncheck, logger, activity_pinger, plextv, pmsconnect
import plexpy.config import plexpy.config
PROG_DIR = None PROG_DIR = None
@ -169,6 +169,7 @@ def initialize(config_file):
# Get the real PMS urls for SSL and remote access # Get the real PMS urls for SSL and remote access
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT: if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
plextv.get_real_pms_url() plextv.get_real_pms_url()
pmsconnect.get_server_friendly_name()
# Refresh the users list on startup # Refresh the users list on startup
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP: if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
@ -280,7 +281,14 @@ def initialize_scheduler():
seconds = 0 seconds = 0
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN: if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs', hours=12, minutes=0, seconds=0) 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',
hours=12, minutes=0, seconds=0)
schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
hours=0, minutes=0, seconds=seconds)
schedule_job(activity_pinger.check_server_response, 'Check for server response',
hours=0, minutes=0, seconds=seconds)
# If we're not using websockets then fall back to polling # If we're not using websockets then fall back to polling
if not CONFIG.MONITORING_USE_WEBSOCKET or POLLING_FAILOVER: if not CONFIG.MONITORING_USE_WEBSOCKET or POLLING_FAILOVER:
@ -552,7 +560,7 @@ def dbcheck():
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, ' 'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, ' 'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER, ' 'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER, '
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER)' 'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER, on_created INTEGER)'
) )
# Upgrade users table from earlier versions # Upgrade users table from earlier versions
@ -588,6 +596,15 @@ def dbcheck():
'ALTER TABLE notify_log ADD COLUMN on_buffer INTEGER' 'ALTER TABLE notify_log ADD COLUMN on_buffer INTEGER'
) )
# Upgrade notify_log table from earlier versions
try:
c_db.execute('SELECT on_created from notify_log')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table notify_log.")
c_db.execute(
'ALTER TABLE notify_log ADD COLUMN on_created INTEGER'
)
# Upgrade sessions table from earlier versions # Upgrade sessions table from earlier versions
try: try:
c_db.execute('SELECT buffer_count from sessions') c_db.execute('SELECT buffer_count from sessions')

View file

@ -1,4 +1,4 @@
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -212,4 +212,54 @@ class ActivityHandler(object):
else: else:
# We don't have this session in our table yet, start a new one. # We don't have this session in our table yet, start a new one.
if this_state != 'buffering': if this_state != 'buffering':
self.on_start() self.on_start()
class TimelineHandler(object):
def __init__(self, timeline):
self.timeline = timeline
#logger.debug(timeline)
def is_item(self):
if 'itemID' in self.timeline:
return True
return False
def get_rating_key(self):
if self.is_item():
return int(self.timeline['itemID'])
return None
def get_metadata(self):
pms_connect = pmsconnect.PmsConnect()
metadata_list = pms_connect.get_metadata_details(self.get_rating_key())
if metadata_list:
return metadata_list['metadata']
return None
def on_created(self):
if self.is_item():
logger.debug(u"PlexPy TimelineHandler :: Library item %s has been added to Plex." % str(self.get_rating_key()))
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(timeline_data=self.get_metadata(), notify_action='created')).start()
# This function receives events from our websocket connection
def process(self):
if self.is_item():
this_state = self.timeline['state']
this_type = self.timeline['type']
this_metadataState = self.timeline.get('metadataState', None)
this_mediaState = self.timeline.get('mediaState', None)
# state: 5: done processing metadata
# type: 1: movie, 2: tv show, 4: episode, 8: artist, 10: track
types = [1, 2, 4, 8, 10]
if this_state == 5 and this_type in types and this_metadataState == None and this_mediaState == None:
self.on_created()

View file

@ -1,4 +1,4 @@
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -13,13 +13,15 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, pmsconnect, notification_handler, database, helpers, activity_processor from plexpy import logger, pmsconnect, plextv, notification_handler, database, helpers, activity_processor
import threading import threading
import plexpy import plexpy
import time import time
monitor_lock = threading.Lock() monitor_lock = threading.Lock()
ext_ping_count = 0
int_ping_count = 0
def check_active_sessions(ws_request=False): def check_active_sessions(ws_request=False):
@ -162,3 +164,109 @@ def check_active_sessions(ws_request=False):
monitor_process.write_session(session) monitor_process.write_session(session)
else: else:
logger.debug(u"PlexPy Monitor :: Unable to read session list.") logger.debug(u"PlexPy Monitor :: Unable to read session list.")
def check_recently_added():
with monitor_lock:
# add delay to allow for metadata processing
delay = plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY
time_threshold = int(time.time()) - delay
time_interval = plexpy.CONFIG.MONITORING_INTERVAL
pms_connect = pmsconnect.PmsConnect()
recently_added_list = pms_connect.get_recently_added_details(count='10')
if recently_added_list:
recently_added = recently_added_list['recently_added']
for item in recently_added:
if item['media_type'] == 'movie':
metadata_list = pms_connect.get_metadata_details(item['rating_key'])
if metadata_list:
metadata = [metadata_list['metadata']]
else:
logger.error(u"PlexPy Monitor :: Unable to retrieve metadata for rating_key %s" \
% str(item['rating_key']))
else:
metadata_list = pms_connect.get_metadata_children_details(item['rating_key'])
if metadata_list:
metadata = metadata_list['metadata']
else:
logger.error(u"PlexPy Monitor :: Unable to retrieve children metadata for rating_key %s" \
% str(item['rating_key']))
if metadata:
if not plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
for item in metadata:
if 0 < int(item['added_at']) - time_threshold <= time_interval:
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(timeline_data=item, notify_action='created')).start()
else:
item = max(metadata, key=lambda x:x['added_at'])
if 0 < int(item['added_at']) - time_threshold <= time_interval:
if item['media_type'] == 'episode' or item['media_type'] == 'track':
metadata_list = pms_connect.get_metadata_details(item['grandparent_rating_key'])
if metadata_list:
item = metadata_list['metadata']
else:
logger.error(u"PlexPy Monitor :: Unable to retrieve grandparent metadata for grandparent_rating_key %s" \
% str(item['rating_key']))
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(timeline_data=item, notify_action='created')).start()
def check_server_response():
with monitor_lock:
pms_connect = pmsconnect.PmsConnect()
server_response = pms_connect.get_server_response()
global int_ping_count
global ext_ping_count
# Check for internal server response
if not server_response:
int_ping_count += 1
logger.warn(u"PlexPy Monitor :: Unable to get an internal response from the server, ping attempt %s." \
% str(int_ping_count))
# Reset internal ping counter
else:
int_ping_count = 0
# Check for remote access
if server_response and plexpy.CONFIG.MONITOR_REMOTE_ACCESS:
mapping_state = server_response['mapping_state']
mapping_error = server_response['mapping_error']
# Check if the port is mapped
if not mapping_state == 'mapped':
ext_ping_count += 1
logger.warn(u"PlexPy Monitor :: Plex remote access port not mapped, ping attempt %s." \
% str(ext_ping_count))
# Check if the port is open
elif mapping_error == 'unreachable':
ext_ping_count += 1
logger.warn(u"PlexPy Monitor :: Plex remote access port mapped, but mapping failed, ping attempt %s." \
% str(ext_ping_count))
# Reset external ping counter
else:
ext_ping_count = 0
if int_ping_count == 3:
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(notify_action='intdown')).start()
if ext_ping_count == 3:
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(notify_action='extdown')).start()

View file

@ -39,6 +39,7 @@ class ActivityProcessor(object):
'parent_title': session['parent_title'], 'parent_title': session['parent_title'],
'grandparent_title': session['grandparent_title'], 'grandparent_title': session['grandparent_title'],
'friendly_name': session['friendly_name'], 'friendly_name': session['friendly_name'],
'ip_address': session['ip_address'],
'player': session['player'], 'player': session['player'],
'platform': session['platform'], 'platform': session['platform'],
'parent_rating_key': session['parent_rating_key'], 'parent_rating_key': session['parent_rating_key'],
@ -78,16 +79,16 @@ class ActivityProcessor(object):
kwargs=dict(stream_data=values, notify_action='play')).start() kwargs=dict(stream_data=values, notify_action='play')).start()
started = int(time.time()) started = int(time.time())
timestamp = {'started': started}
# Try and grab IP address from logs # Try and grab IP address from logs (fallback if not on PMS 0.9.14 and above)
if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER: if not session['ip_address']:
ip_address = self.find_session_ip(rating_key=session['rating_key'], if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER:
machine_id=session['machine_id']) ip_address = self.find_session_ip(rating_key=session['rating_key'],
else: machine_id=session['machine_id'])
ip_address = None timestamp.update({'ip_address': ip_address})
else:
timestamp = {'started': started, timestamp.update({'ip_address': None})
'ip_address': ip_address}
# If it's our first write then time stamp it. # If it's our first write then time stamp it.
self.db.upsert('sessions', timestamp, keys) self.db.upsert('sessions', timestamp, keys)
@ -109,8 +110,11 @@ class ActivityProcessor(object):
else: else:
stopped = int(time.time()) stopped = int(time.time())
if plexpy.CONFIG.VIDEO_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \ if plexpy.CONFIG.MOVIE_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
(session['media_type'] == 'movie' or session['media_type'] == 'episode'): session['media_type'] == 'movie':
logging_enabled = True
elif plexpy.CONFIG.TV_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
session['media_type'] == 'episode':
logging_enabled = True logging_enabled = True
elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \ elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
session['media_type'] == 'track': session['media_type'] == 'track':
@ -278,10 +282,17 @@ class ActivityProcessor(object):
if ipv4: if ipv4:
# The logged IP will always be the first match and we don't want localhost entries # The logged IP will always be the first match and we don't want localhost entries
if ipv4[0] != '127.0.0.1': if ipv4[0] != '127.0.0.1':
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s " # check if IPv4 mapped IPv6 address (::ffff:xxx.xxx.xxx.xxx)
u"and machineIdentifier %s." if '::ffff:' + ipv4[0] in line:
% (ipv4[0], rating_key, machine_id)) logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s "
return ipv4[0] u"and machineIdentifier %s."
% ('::ffff:' + ipv4[0], rating_key, machine_id))
return '::ffff:' + ipv4[0]
else:
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s "
u"and machineIdentifier %s."
% (ipv4[0], rating_key, machine_id))
return ipv4[0]
logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on first pass. " logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on first pass. "
u"Attempting fallback check in 5 seconds...") u"Attempting fallback check in 5 seconds...")
@ -301,9 +312,14 @@ class ActivityProcessor(object):
if ipv4: if ipv4:
# The logged IP will always be the first match and we don't want localhost entries # The logged IP will always be the first match and we don't want localhost entries
if ipv4[0] != '127.0.0.1': if ipv4[0] != '127.0.0.1':
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." % if '::ffff:' + ipv4[0] in line:
(ipv4[0], rating_key)) logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." %
return ipv4[0] ('::ffff:' + ipv4[0], rating_key))
return '::ffff:' + ipv4[0]
else:
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." %
(ipv4[0], rating_key))
return ipv4[0]
logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on fallback search. Not logging IP address.") logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on fallback search. Not logging IP address.")

View file

@ -394,8 +394,8 @@ class Api(object):
"grouping_global_history": bool(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY), "grouping_global_history": bool(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
"grouping_user_history": bool(plexpy.CONFIG.GROUPING_USER_HISTORY), "grouping_user_history": bool(plexpy.CONFIG.GROUPING_USER_HISTORY),
"grouping_charts": bool(plexpy.CONFIG.GROUPING_CHARTS), "grouping_charts": bool(plexpy.CONFIG.GROUPING_CHARTS),
"tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": bool(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE), "movie_notify_enable": bool(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"music_notify_enable": bool(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE), "music_notify_enable": bool(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": bool(plexpy.CONFIG.TV_NOTIFY_ON_START), "tv_notify_on_start": bool(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_START), "movie_notify_on_start": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
@ -410,7 +410,8 @@ class Api(object):
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL, "refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
"refresh_users_on_startup": bool(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP), "refresh_users_on_startup": bool(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
"ip_logging_enable": bool(plexpy.CONFIG.IP_LOGGING_ENABLE), "ip_logging_enable": bool(plexpy.CONFIG.IP_LOGGING_ENABLE),
"video_logging_enable": bool(plexpy.CONFIG.VIDEO_LOGGING_ENABLE), "movie_logging_enable": bool(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
"tv_logging_enable": bool(plexpy.CONFIG.TV_LOGGING_ENABLE),
"music_logging_enable": bool(plexpy.CONFIG.MUSIC_LOGGING_ENABLE), "music_logging_enable": bool(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL, "logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"pms_is_remote": bool(plexpy.CONFIG.PMS_IS_REMOTE), "pms_is_remote": bool(plexpy.CONFIG.PMS_IS_REMOTE),

View file

@ -41,4 +41,9 @@ notify_strings[NOTIFY_STOPPED] = "Playback stopped"
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png" DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png" DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png" DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
PLATFORM_NAME_OVERRIDES = {'Konvergo': 'Plex Media Player',
'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}

View file

@ -26,6 +26,7 @@ _CONFIG_DEFINITIONS = {
'PMS_IP': (str, 'PMS', '127.0.0.1'), 'PMS_IP': (str, 'PMS', '127.0.0.1'),
'PMS_IS_REMOTE': (int, 'PMS', 0), 'PMS_IS_REMOTE': (int, 'PMS', 0),
'PMS_LOGS_FOLDER': (str, 'PMS', ''), 'PMS_LOGS_FOLDER': (str, 'PMS', ''),
'PMS_NAME': (unicode, 'PMS', ''),
'PMS_PORT': (int, 'PMS', 32400), 'PMS_PORT': (int, 'PMS', 32400),
'PMS_TOKEN': (str, 'PMS', ''), 'PMS_TOKEN': (str, 'PMS', ''),
'PMS_SSL': (int, 'General', 0), 'PMS_SSL': (int, 'General', 0),
@ -44,6 +45,9 @@ _CONFIG_DEFINITIONS = {
'BOXCAR_ON_RESUME': (int, 'Boxcar', 0), 'BOXCAR_ON_RESUME': (int, 'Boxcar', 0),
'BOXCAR_ON_BUFFER': (int, 'Boxcar', 0), 'BOXCAR_ON_BUFFER': (int, 'Boxcar', 0),
'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0), 'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0),
'BOXCAR_ON_CREATED': (int, 'Boxcar', 0),
'BOXCAR_ON_EXTDOWN': (int, 'Boxcar', 0),
'BOXCAR_ON_INTDOWN': (int, 'Boxcar', 0),
'BUFFER_THRESHOLD': (int, 'Monitoring', 3), 'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
'BUFFER_WAIT': (int, 'Monitoring', 900), 'BUFFER_WAIT': (int, 'Monitoring', 900),
'CACHE_DIR': (str, 'General', ''), 'CACHE_DIR': (str, 'General', ''),
@ -68,6 +72,9 @@ _CONFIG_DEFINITIONS = {
'EMAIL_ON_RESUME': (int, 'Email', 0), 'EMAIL_ON_RESUME': (int, 'Email', 0),
'EMAIL_ON_BUFFER': (int, 'Email', 0), 'EMAIL_ON_BUFFER': (int, 'Email', 0),
'EMAIL_ON_WATCHED': (int, 'Email', 0), 'EMAIL_ON_WATCHED': (int, 'Email', 0),
'EMAIL_ON_CREATED': (int, 'Email', 0),
'EMAIL_ON_EXTDOWN': (int, 'Email', 0),
'EMAIL_ON_INTDOWN': (int, 'Email', 0),
'ENABLE_HTTPS': (int, 'General', 0), 'ENABLE_HTTPS': (int, 'General', 0),
'FIRST_RUN_COMPLETE': (int, 'General', 0), 'FIRST_RUN_COMPLETE': (int, 'General', 0),
'FREEZE_DB': (int, 'General', 0), 'FREEZE_DB': (int, 'General', 0),
@ -84,6 +91,9 @@ _CONFIG_DEFINITIONS = {
'GROWL_ON_RESUME': (int, 'Growl', 0), 'GROWL_ON_RESUME': (int, 'Growl', 0),
'GROWL_ON_BUFFER': (int, 'Growl', 0), 'GROWL_ON_BUFFER': (int, 'Growl', 0),
'GROWL_ON_WATCHED': (int, 'Growl', 0), 'GROWL_ON_WATCHED': (int, 'Growl', 0),
'GROWL_ON_CREATED': (int, 'Growl', 0),
'GROWL_ON_EXTDOWN': (int, 'Growl', 0),
'GROWL_ON_INTDOWN': (int, 'Growl', 0),
'HOME_LIBRARY_CARDS': (str, 'General', 'library_statistics_first'), 'HOME_LIBRARY_CARDS': (str, 'General', 'library_statistics_first'),
'HOME_STATS_LENGTH': (int, 'General', 30), 'HOME_STATS_LENGTH': (int, 'General', 30),
'HOME_STATS_TYPE': (int, 'General', 0), 'HOME_STATS_TYPE': (int, 'General', 0),
@ -99,19 +109,33 @@ _CONFIG_DEFINITIONS = {
'HTTP_USERNAME': (str, 'General', ''), 'HTTP_USERNAME': (str, 'General', ''),
'INTERFACE': (str, 'General', 'default'), 'INTERFACE': (str, 'General', 'default'),
'IP_LOGGING_ENABLE': (int, 'General', 0), 'IP_LOGGING_ENABLE': (int, 'General', 0),
'IFTTT_KEY': (str, 'IFTTT', ''),
'IFTTT_EVENT': (str, 'IFTTT', 'plexpy'),
'IFTTT_ENABLED': (int, 'IFTTT', 0),
'IFTTT_ON_PLAY': (int, 'IFTTT', 0),
'IFTTT_ON_STOP': (int, 'IFTTT', 0),
'IFTTT_ON_PAUSE': (int, 'IFTTT', 0),
'IFTTT_ON_RESUME': (int, 'IFTTT', 0),
'IFTTT_ON_BUFFER': (int, 'IFTTT', 0),
'IFTTT_ON_WATCHED': (int, 'IFTTT', 0),
'IFTTT_ON_CREATED': (int, 'IFTTT', 0),
'IFTTT_ON_EXTDOWN': (int, 'IFTTT', 0),
'IFTTT_ON_INTDOWN': (int, 'IFTTT', 0),
'JOURNAL_MODE': (str, 'Advanced', 'wal'), 'JOURNAL_MODE': (str, 'Advanced', 'wal'),
'LAUNCH_BROWSER': (int, 'General', 1), 'LAUNCH_BROWSER': (int, 'General', 1),
'LOG_DIR': (str, 'General', ''), 'LOG_DIR': (str, 'General', ''),
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120), 'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0), 'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1), 'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ON_STOP': (int, 'Monitoring', 0), 'MOVIE_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'MOVIE_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0), 'MOVIE_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MUSIC_NOTIFY_ENABLE': (int, 'Monitoring', 0), 'MUSIC_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1), 'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1),
'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0), 'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0), 'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 0), 'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
'MONITORING_INTERVAL': (int, 'Monitoring', 60), 'MONITORING_INTERVAL': (int, 'Monitoring', 60),
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0), 'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
'NMA_APIKEY': (str, 'NMA', ''), 'NMA_APIKEY': (str, 'NMA', ''),
@ -123,7 +147,12 @@ _CONFIG_DEFINITIONS = {
'NMA_ON_RESUME': (int, 'NMA', 0), 'NMA_ON_RESUME': (int, 'NMA', 0),
'NMA_ON_BUFFER': (int, 'NMA', 0), 'NMA_ON_BUFFER': (int, 'NMA', 0),
'NMA_ON_WATCHED': (int, 'NMA', 0), 'NMA_ON_WATCHED': (int, 'NMA', 0),
'NMA_ON_CREATED': (int, 'NMA', 0),
'NMA_ON_EXTDOWN': (int, 'NMA', 0),
'NMA_ON_INTDOWN': (int, 'NMA', 0),
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1), 'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85), 'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
'NOTIFY_ON_START_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'), 'NOTIFY_ON_START_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_START_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) started playing {title}.'), 'NOTIFY_ON_START_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) started playing {title}.'),
@ -137,6 +166,12 @@ _CONFIG_DEFINITIONS = {
'NOTIFY_ON_BUFFER_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) is buffering {title}.'), 'NOTIFY_ON_BUFFER_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) is buffering {title}.'),
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'), 'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_WATCHED_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has watched {title}.'), 'NOTIFY_ON_WATCHED_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has watched {title}.'),
'NOTIFY_ON_CREATED_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_CREATED_BODY_TEXT': (str, 'Monitoring', '{title} was recently added to Plex.'),
'NOTIFY_ON_EXTDOWN_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_EXTDOWN_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server remote access is down.'),
'NOTIFY_ON_INTDOWN_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_INTDOWN_BODY_TEXT': (str, 'Monitoring', 'The Plex Media Server is down.'),
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'), 'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
@ -145,6 +180,9 @@ _CONFIG_DEFINITIONS = {
'OSX_NOTIFY_ON_RESUME': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ON_RESUME': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_BUFFER': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ON_BUFFER': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0), 'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_CREATED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_EXTDOWN': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_INTDOWN': (int, 'OSX_Notify', 0),
'PLEX_CLIENT_HOST': (str, 'Plex', ''), 'PLEX_CLIENT_HOST': (str, 'Plex', ''),
'PLEX_ENABLED': (int, 'Plex', 0), 'PLEX_ENABLED': (int, 'Plex', 0),
'PLEX_PASSWORD': (str, 'Plex', ''), 'PLEX_PASSWORD': (str, 'Plex', ''),
@ -155,6 +193,9 @@ _CONFIG_DEFINITIONS = {
'PLEX_ON_RESUME': (int, 'Plex', 0), 'PLEX_ON_RESUME': (int, 'Plex', 0),
'PLEX_ON_BUFFER': (int, 'Plex', 0), 'PLEX_ON_BUFFER': (int, 'Plex', 0),
'PLEX_ON_WATCHED': (int, 'Plex', 0), 'PLEX_ON_WATCHED': (int, 'Plex', 0),
'PLEX_ON_CREATED': (int, 'Plex', 0),
'PLEX_ON_EXTDOWN': (int, 'Plex', 0),
'PLEX_ON_INTDOWN': (int, 'Plex', 0),
'PROWL_ENABLED': (int, 'Prowl', 0), 'PROWL_ENABLED': (int, 'Prowl', 0),
'PROWL_KEYS': (str, 'Prowl', ''), 'PROWL_KEYS': (str, 'Prowl', ''),
'PROWL_PRIORITY': (int, 'Prowl', 0), 'PROWL_PRIORITY': (int, 'Prowl', 0),
@ -164,6 +205,9 @@ _CONFIG_DEFINITIONS = {
'PROWL_ON_RESUME': (int, 'Prowl', 0), 'PROWL_ON_RESUME': (int, 'Prowl', 0),
'PROWL_ON_BUFFER': (int, 'Prowl', 0), 'PROWL_ON_BUFFER': (int, 'Prowl', 0),
'PROWL_ON_WATCHED': (int, 'Prowl', 0), 'PROWL_ON_WATCHED': (int, 'Prowl', 0),
'PROWL_ON_CREATED': (int, 'Prowl', 0),
'PROWL_ON_EXTDOWN': (int, 'Prowl', 0),
'PROWL_ON_INTDOWN': (int, 'Prowl', 0),
'PUSHALOT_APIKEY': (str, 'Pushalot', ''), 'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
'PUSHALOT_ENABLED': (int, 'Pushalot', 0), 'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0), 'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
@ -172,6 +216,9 @@ _CONFIG_DEFINITIONS = {
'PUSHALOT_ON_RESUME': (int, 'Pushalot', 0), 'PUSHALOT_ON_RESUME': (int, 'Pushalot', 0),
'PUSHALOT_ON_BUFFER': (int, 'Pushalot', 0), 'PUSHALOT_ON_BUFFER': (int, 'Pushalot', 0),
'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0), 'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0),
'PUSHALOT_ON_CREATED': (int, 'Pushalot', 0),
'PUSHALOT_ON_EXTDOWN': (int, 'Pushalot', 0),
'PUSHALOT_ON_INTDOWN': (int, 'Pushalot', 0),
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''), 'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''), 'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''), 'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''),
@ -182,6 +229,9 @@ _CONFIG_DEFINITIONS = {
'PUSHBULLET_ON_RESUME': (int, 'PushBullet', 0), 'PUSHBULLET_ON_RESUME': (int, 'PushBullet', 0),
'PUSHBULLET_ON_BUFFER': (int, 'PushBullet', 0), 'PUSHBULLET_ON_BUFFER': (int, 'PushBullet', 0),
'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0), 'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_CREATED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_EXTDOWN': (int, 'PushBullet', 0),
'PUSHBULLET_ON_INTDOWN': (int, 'PushBullet', 0),
'PUSHOVER_APITOKEN': (str, 'Pushover', ''), 'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
'PUSHOVER_ENABLED': (int, 'Pushover', 0), 'PUSHOVER_ENABLED': (int, 'Pushover', 0),
'PUSHOVER_KEYS': (str, 'Pushover', ''), 'PUSHOVER_KEYS': (str, 'Pushover', ''),
@ -193,8 +243,24 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_ON_RESUME': (int, 'Pushover', 0), 'PUSHOVER_ON_RESUME': (int, 'Pushover', 0),
'PUSHOVER_ON_BUFFER': (int, 'Pushover', 0), 'PUSHOVER_ON_BUFFER': (int, 'Pushover', 0),
'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0), 'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0),
'PUSHOVER_ON_CREATED': (int, 'Pushover', 0),
'PUSHOVER_ON_EXTDOWN': (int, 'Pushover', 0),
'PUSHOVER_ON_INTDOWN': (int, 'Pushover', 0),
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12), 'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1), 'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
'TELEGRAM_CHAT_ID': (int, 'Telegram', 0),
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
'TELEGRAM_ON_RESUME': (int, 'Telegram', 0),
'TELEGRAM_ON_BUFFER': (int, 'Telegram', 0),
'TELEGRAM_ON_WATCHED': (int, 'Telegram', 0),
'TELEGRAM_ON_CREATED': (int, 'Telegram', 0),
'TELEGRAM_ON_EXTDOWN': (int, 'Telegram', 0),
'TELEGRAM_ON_INTDOWN': (int, 'Telegram', 0),
'TV_LOGGING_ENABLE': (int, 'Monitoring', 1),
'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0), 'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'TV_NOTIFY_ON_START': (int, 'Monitoring', 1), 'TV_NOTIFY_ON_START': (int, 'Monitoring', 1),
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0), 'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
@ -209,6 +275,9 @@ _CONFIG_DEFINITIONS = {
'TWITTER_ON_RESUME': (int, 'Twitter', 0), 'TWITTER_ON_RESUME': (int, 'Twitter', 0),
'TWITTER_ON_BUFFER': (int, 'Twitter', 0), 'TWITTER_ON_BUFFER': (int, 'Twitter', 0),
'TWITTER_ON_WATCHED': (int, 'Twitter', 0), 'TWITTER_ON_WATCHED': (int, 'Twitter', 0),
'TWITTER_ON_CREATED': (int, 'Twitter', 0),
'TWITTER_ON_EXTDOWN': (int, 'Twitter', 0),
'TWITTER_ON_INTDOWN': (int, 'Twitter', 0),
'UPDATE_DB_INTERVAL': (int, 'General', 24), 'UPDATE_DB_INTERVAL': (int, 'General', 24),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1), 'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1), 'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
@ -221,7 +290,10 @@ _CONFIG_DEFINITIONS = {
'XBMC_ON_PAUSE': (int, 'XBMC', 0), 'XBMC_ON_PAUSE': (int, 'XBMC', 0),
'XBMC_ON_RESUME': (int, 'XBMC', 0), 'XBMC_ON_RESUME': (int, 'XBMC', 0),
'XBMC_ON_BUFFER': (int, 'XBMC', 0), 'XBMC_ON_BUFFER': (int, 'XBMC', 0),
'XBMC_ON_WATCHED': (int, 'XBMC', 0) 'XBMC_ON_WATCHED': (int, 'XBMC', 0),
'XBMC_ON_CREATED': (int, 'XBMC', 0),
'XBMC_ON_EXTDOWN': (int, 'XBMC', 0),
'XBMC_ON_INTDOWN': (int, 'XBMC', 0)
} }
# pylint:disable=R0902 # pylint:disable=R0902
# it might be nice to refactor for fewer instance variables # it might be nice to refactor for fewer instance variables
@ -234,6 +306,7 @@ class Config(object):
self._config = ConfigObj(self._config_file, encoding='utf-8') self._config = ConfigObj(self._config_file, encoding='utf-8')
for key in _CONFIG_DEFINITIONS.keys(): for key in _CONFIG_DEFINITIONS.keys():
self.check_setting(key) self.check_setting(key)
self._upgrade()
def _define(self, name): def _define(self, name):
key = name.upper() key = name.upper()
@ -322,4 +395,18 @@ class Config(object):
""" """
for name, value in kwargs.items(): for name, value in kwargs.items():
key, definition_type, section, ini_key, default = self._define(name) key, definition_type, section, ini_key, default = self._define(name)
self._config[section][ini_key] = definition_type(value) self._config[section][ini_key] = definition_type(value)
def _upgrade(self):
"""
Upgrades config file from previous verisions and bumps up config version
"""
if self.CONFIG_VERSION == '0':
# Separate out movie and tv notifications
if self.MOVIE_NOTIFY_ENABLE == 1:
self.TV_NOTIFY_ENABLE = 1
# Separate out movie and tv logging
if self.VIDEO_LOGGING_ENABLE == 0:
self.MOVIE_LOGGING_ENABLE = 0
self.TV_LOGGING_ENABLE = 0
self.CONFIG_VERSION = '1'

View file

@ -41,7 +41,7 @@ class DataFactory(object):
'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS paused_counter', 'SUM(CASE WHEN paused_counter IS NULL THEN 0 ELSE paused_counter END) AS paused_counter',
'session_history.user_id', 'session_history.user_id',
'session_history.user', 'session_history.user',
'(CASE WHEN users.friendly_name IS NULL THEN user ELSE users.friendly_name END) as friendly_name', '(CASE WHEN users.friendly_name IS NULL THEN users.username ELSE users.friendly_name END) as friendly_name',
'platform', 'platform',
'player', 'player',
'ip_address', 'ip_address',
@ -106,10 +106,7 @@ class DataFactory(object):
watched_status = 0 watched_status = 0
# Rename Mystery platform names # Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3', platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
row = {"reference_id": item["reference_id"], row = {"reference_id": item["reference_id"],
"id": item["id"], "id": item["id"],
@ -182,7 +179,7 @@ class DataFactory(object):
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: top_tv.")
return None return None
for item in result: for item in result:
@ -230,7 +227,7 @@ class DataFactory(object):
'LIMIT %s' % (time_range, sort_type, stats_count) 'LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: popular_tv.")
return None return None
for item in result: for item in result:
@ -274,7 +271,7 @@ class DataFactory(object):
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: top_movies.")
return None return None
for item in result: for item in result:
@ -322,7 +319,7 @@ class DataFactory(object):
'LIMIT %s' % (time_range, sort_type, stats_count) 'LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: popular_movies.")
return None return None
for item in result: for item in result:
@ -366,7 +363,7 @@ class DataFactory(object):
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: top_music.")
return None return None
for item in result: for item in result:
@ -414,7 +411,7 @@ class DataFactory(object):
'LIMIT %s' % (time_range, sort_type, stats_count) 'LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: popular_music.")
return None return None
for item in result: for item in result:
@ -440,7 +437,7 @@ class DataFactory(object):
top_users = [] top_users = []
try: try:
query = 'SELECT session_history.user, ' \ query = 'SELECT session_history.user, ' \
'(case when users.friendly_name is null then session_history.user else ' \ '(case when users.friendly_name is null then users.username else ' \
'users.friendly_name end) as friendly_name,' \ 'users.friendly_name end) as friendly_name,' \
'COUNT(session_history.id) as total_plays, ' \ 'COUNT(session_history.id) as total_plays, ' \
'SUM(case when session_history.stopped > 0 ' \ 'SUM(case when session_history.stopped > 0 ' \
@ -459,7 +456,7 @@ class DataFactory(object):
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: top_users.")
return None return None
for item in result: for item in result:
@ -507,15 +504,12 @@ class DataFactory(object):
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: top_platforms.")
return None return None
for item in result: for item in result:
# Rename Mystery platform names # Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3', platform_type = common.PLATFORM_NAME_OVERRIDES.get(item[0], item[0])
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform_type = platform_names.get(item[0], item[0])
row = {'platform': item[0], row = {'platform': item[0],
'total_plays': item[1], 'total_plays': item[1],
@ -542,7 +536,7 @@ class DataFactory(object):
try: try:
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
'session_history.user, ' \ 'session_history.user, ' \
'(case when users.friendly_name is null then session_history.user else ' \ '(case when users.friendly_name is null then users.username else ' \
'users.friendly_name end) as friendly_name,' \ 'users.friendly_name end) as friendly_name,' \
'users.user_id, ' \ 'users.user_id, ' \
'users.custom_avatar_url as user_thumb, ' \ 'users.custom_avatar_url as user_thumb, ' \
@ -564,12 +558,12 @@ class DataFactory(object):
'AND (session_history_metadata.media_type = "movie" ' \ 'AND (session_history_metadata.media_type = "movie" ' \
'OR session_history_metadata.media_type = "episode") ' \ 'OR session_history_metadata.media_type = "episode") ' \
'AND percent_complete >= %s ' \ 'AND percent_complete >= %s ' \
'GROUP BY session_history_metadata.full_title ' \ 'GROUP BY session_history.id ' \
'ORDER BY last_watch DESC ' \ 'ORDER BY last_watch DESC ' \
'LIMIT %s' % (time_range, notify_watched_percent, stats_count) 'LIMIT %s' % (time_range, notify_watched_percent, stats_count)
result = monitor_db.select(query) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_home_stats: last_watched.")
return None return None
for item in result: for item in result:
@ -680,7 +674,7 @@ class DataFactory(object):
'ORDER BY started DESC LIMIT ?' 'ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[limit]) result = monitor_db.select(query, args=[limit])
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_recently_watched.")
return None return None
for row in result: for row in result:
@ -729,7 +723,7 @@ class DataFactory(object):
actors = item['actors'].split(';') if item['actors'] else [] actors = item['actors'].split(';') if item['actors'] else []
genres = item['genres'].split(';') if item['genres'] else [] genres = item['genres'].split(';') if item['genres'] else []
metadata = {'type': item['media_type'], metadata = {'media_type': item['media_type'],
'rating_key': item['rating_key'], 'rating_key': item['rating_key'],
'parent_rating_key': item['parent_rating_key'], 'parent_rating_key': item['parent_rating_key'],
'grandparent_rating_key': item['grandparent_rating_key'], 'grandparent_rating_key': item['grandparent_rating_key'],
@ -854,7 +848,7 @@ class DataFactory(object):
media_type = 'artist' media_type = 'artist'
if query_string and media_type: if query_string and media_type:
query = {'query_string': query_string.replace('"', ''), query = {'query_string': query_string,
'title': title, 'title': title,
'parent_title': parent_title, 'parent_title': parent_title,
'grandparent_title': grandparent_title, 'grandparent_title': grandparent_title,
@ -894,7 +888,7 @@ class DataFactory(object):
grandparent_rating_key = result[0]['grandparent_rating_key'] grandparent_rating_key = result[0]['grandparent_rating_key']
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query for get_rating_keys_list.")
return {} return {}
query = 'SELECT rating_key, parent_rating_key, grandparent_rating_key, title, parent_title, grandparent_title, ' \ query = 'SELECT rating_key, parent_rating_key, grandparent_rating_key, title, parent_title, grandparent_title, ' \
@ -1004,4 +998,20 @@ class DataFactory(object):
else: else:
return 'No updated rating key needed in database. No changes were made.' return 'No updated rating key needed in database. No changes were made.'
# for debugging # for debugging
#return mapping #return mapping
def get_session_ip(self, session_key=''):
monitor_db = database.MonitorDatabase()
if session_key:
query = 'SELECT ip_address FROM sessions WHERE session_key = %d' % int(session_key)
result = monitor_db.select(query)
else:
return None
ip_address = 'N/A'
for item in result:
ip_address = item[0]
return ip_address

View file

@ -1,4 +1,4 @@
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -70,6 +70,39 @@ class DataTables(object):
else: else:
grouping = False grouping = False
# Build join parameters
if join_types:
counter = 0
for join_type in join_types:
if join_type.upper() == 'LEFT OUTER JOIN':
join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN':
join_item = 'JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
else:
join_item = ''
counter += 1
join += join_item
# Build custom where parameters
if custom_where:
for w in custom_where:
c_where += w[0] + ' = ? AND '
# The order of our args changes if we are grouping
#if grouping:
# args.insert(0, w[1])
#else:
# args.append(w[1])
# My testing shows that order of args doesn't change
args.append(w[1])
if c_where:
c_where = 'WHERE ' + c_where.rstrip(' AND ')
# Build ordering # Build ordering
for o in parameters['order']: for o in parameters['order']:
sort_order = ' COLLATE NOCASE' sort_order = ' COLLATE NOCASE'
@ -119,36 +152,6 @@ class DataTables(object):
if where: if where:
where = 'WHERE ' + where.rstrip(' OR ') where = 'WHERE ' + where.rstrip(' OR ')
# Build join parameters
if join_types:
counter = 0
for join_type in join_types:
if join_type.upper() == 'LEFT OUTER JOIN':
join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN':
join_item = 'JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
else:
join_item = ''
counter += 1
join += join_item
# Build custom where parameters
if custom_where:
for w in custom_where:
c_where += w[0] + ' = ? AND '
# The order of our args changes if we are grouping
if grouping:
args.insert(0, w[1])
else:
args.append(w[1])
if c_where:
c_where = 'WHERE ' + c_where.rstrip(' AND ')
# Build our queries # Build our queries
if grouping: if grouping:
if c_where == '': if c_where == '':

View file

@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, database, helpers from plexpy import logger, database, helpers, common
import datetime import datetime
@ -386,18 +386,11 @@ class Graphs(object):
series_3 = [] series_3 = []
for item in result: for item in result:
categories.append(item[0]) categories.append(common.PLATFORM_NAME_OVERRIDES.get(item[0], item[0]))
series_1.append(item[1]) series_1.append(item[1])
series_2.append(item[2]) series_2.append(item[2])
series_3.append(item[3]) series_3.append(item[3])
# Rename Mystery platform names
platform_names = [('Mystery 3', 'Playstation 3'),
('Mystery 4', 'Playstation 4'),
('Mystery 5', 'Xbox 360')]
for old_name, new_name in platform_names:
categories = [item.replace(old_name, new_name) for item in categories]
series_1_output = {'name': 'TV', series_1_output = {'name': 'TV',
'data': series_1} 'data': series_1}
series_2_output = {'name': 'Movies', series_2_output = {'name': 'Movies',
@ -417,7 +410,7 @@ class Graphs(object):
if y_axis == 'plays': if y_axis == 'plays':
query = 'SELECT ' \ query = 'SELECT ' \
'(case when users.friendly_name is null then session_history.user else ' \ '(case when users.friendly_name is null then users.username else ' \
'users.friendly_name end) as friendly_name,' \ 'users.friendly_name end) as friendly_name,' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \ 'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \ 'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
@ -434,7 +427,7 @@ class Graphs(object):
result = monitor_db.select(query) result = monitor_db.select(query)
else: else:
query = 'SELECT ' \ query = 'SELECT ' \
'(case when users.friendly_name is null then session_history.user else ' \ '(case when users.friendly_name is null then users.username else ' \
'users.friendly_name end) as friendly_name,' \ 'users.friendly_name end) as friendly_name,' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \ 'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \ ' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
@ -808,18 +801,11 @@ class Graphs(object):
series_3 = [] series_3 = []
for item in result: for item in result:
categories.append(item[0]) categories.append(common.PLATFORM_NAME_OVERRIDES.get(item[0], item[0]))
series_1.append(item[1]) series_1.append(item[1])
series_2.append(item[2]) series_2.append(item[2])
series_3.append(item[3]) series_3.append(item[3])
# Rename Mystery platform names
platform_names = [('Mystery 3', 'Playstation 3'),
('Mystery 4', 'Playstation 4'),
('Mystery 5', 'Xbox 360')]
for old_name, new_name in platform_names:
categories = [item.replace(old_name, new_name) for item in categories]
series_1_output = {'name': 'Direct Play', series_1_output = {'name': 'Direct Play',
'data': series_1} 'data': series_1}
series_2_output = {'name': 'Direct Stream', series_2_output = {'name': 'Direct Stream',

View file

@ -1,4 +1,4 @@
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -144,6 +144,31 @@ def now():
now = datetime.datetime.now() now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S") return now.strftime("%Y-%m-%d %H:%M:%S")
def human_duration(s):
hd = ''
if str(s).isdigit():
d = int(s / 84600)
h = int((s % 84600) / 3600)
m = int(((s % 84600) % 3600) / 60)
s = int(((s % 84600) % 3600) % 60)
hd_list = []
if d > 0:
hd_list.append(str(d) + ' days')
if h > 0:
hd_list.append(str(h) + ' hrs')
if m > 0:
hd_list.append(str(m) + ' mins')
if s > 0:
hd_list.append(str(s) + ' secs')
hd = ' '.join(hd_list)
return hd
else:
return hd
def get_age(date): def get_age(date):

View file

@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, config, notifiers, database, helpers from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect
import plexpy import plexpy
import time import time
@ -30,134 +30,133 @@ def notify(stream_data=None, notify_action=None):
if not user_details['do_notify']: if not user_details['do_notify']:
return return
if stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode': if (stream_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
if plexpy.CONFIG.MOVIE_NOTIFY_ENABLE or plexpy.CONFIG.TV_NOTIFY_ENABLE: or (stream_data['media_type'] == 'episode' and plexpy.CONFIG.TV_NOTIFY_ENABLE):
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration']) progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
for agent in notifiers.available_notification_agents(): for agent in notifiers.available_notification_agents():
if agent['on_play'] and notify_action == 'play': 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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_stop'] and notify_action == 'stop' \
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'],
subject=notify_strings[0],
body=notify_strings[1])
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_pause'] and notify_action == 'pause' \
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'],
subject=notify_strings[0],
body=notify_strings[1])
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_resume'] and notify_action == 'resume' \
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'],
subject=notify_strings[0],
body=notify_strings[1])
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'],
subject=notify_strings[0],
body=notify_strings[1])
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_watched'] and notify_action == 'watched':
# Get the current states for notifications from our db
notify_states = get_notify_state(session=stream_data)
# If there is nothing in the notify_log for our agent id but it is enabled we should notify
if not any(d['agent_id'] == agent['id'] for d in notify_states):
# Build and send notification # Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action) notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'], notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0], subject=notify_strings[0],
body=notify_strings[1]) body=notify_strings[1])
# Set the notification state in the db # Set the notification state in the db
set_notify_state(session=stream_data, state='play', agent_info=agent) set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_stop'] and notify_action == 'stop' \ else:
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT): # Check in our notify log if the notification has already been sent
# Build and send notification for notify_state in notify_states:
notify_strings = build_notify_text(session=stream_data, state=notify_action) if not notify_state['on_watched'] and (notify_state['agent_id'] == agent['id']):
notifiers.send_notification(config_id=agent['id'], # Build and send notification
subject=notify_strings[0], notify_strings = build_notify_text(session=stream_data, state=notify_action)
body=notify_strings[1]) notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
set_notify_state(session=stream_data, state='stop', agent_info=agent) elif (stream_data['media_type'] == 'track' and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
elif agent['on_pause'] and notify_action == 'pause' \ for agent in notifiers.available_notification_agents():
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99): if agent['on_play'] and notify_action == 'play':
# Build and send notification # Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action) notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'], notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0], subject=notify_strings[0],
body=notify_strings[1]) body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
set_notify_state(session=stream_data, state='pause', agent_info=agent) 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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_resume'] and notify_action == 'resume' \ elif agent['on_pause'] and notify_action == 'pause':
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99): # Build and send notification
# Build and send notification notify_strings = build_notify_text(session=stream_data, state=notify_action)
notify_strings = build_notify_text(session=stream_data, state=notify_action) notifiers.send_notification(config_id=agent['id'],
notifiers.send_notification(config_id=agent['id'], subject=notify_strings[0],
subject=notify_strings[0], body=notify_strings[1])
body=notify_strings[1]) # Set the notification state in the db
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
set_notify_state(session=stream_data, state='resume', agent_info=agent) 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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_buffer'] and notify_action == 'buffer': elif agent['on_buffer'] and notify_action == 'buffer':
# Build and send notification # Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action) notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'], notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0], subject=notify_strings[0],
body=notify_strings[1]) body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='buffer', agent_info=agent) set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_watched'] and notify_action == 'watched':
# Get the current states for notifications from our db
notify_states = get_notify_state(session=stream_data)
# If there is nothing in the notify_log for our agent id but it is enabled we should notify
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='watched', agent_info=agent)
else:
# Check in our notify log if the notification has already been sent
for notify_state in notify_states:
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='watched', agent_info=agent)
elif stream_data['media_type'] == 'track':
if plexpy.CONFIG.MUSIC_NOTIFY_ENABLE:
for agent in notifiers.available_notification_agents():
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='play', agent_info=agent)
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='stop', agent_info=agent)
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='pause', agent_info=agent)
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='resume', 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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='buffer', agent_info=agent)
elif stream_data['media_type'] == 'clip': elif stream_data['media_type'] == 'clip':
pass pass
@ -168,6 +167,42 @@ def notify(stream_data=None, notify_action=None):
logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.") logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.")
def notify_timeline(timeline_data=None, notify_action=None):
if timeline_data and notify_action:
if (timeline_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
or ((timeline_data['media_type'] == 'show' or timeline_data['media_type'] == 'episode') \
and plexpy.CONFIG.TV_NOTIFY_ENABLE) \
or ((timeline_data['media_type'] == 'artist' or timeline_data['media_type'] == 'track') \
and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
for agent in notifiers.available_notification_agents():
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'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=timeline_data, state=notify_action, agent_info=agent)
elif not timeline_data and notify_action:
for agent in notifiers.available_notification_agents():
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'],
subject=notify_strings[0],
body=notify_strings[1])
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'],
subject=notify_strings[0],
body=notify_strings[1])
else:
logger.debug(u"PlexPy Notifier :: Notify timeline called but incomplete data received.")
def get_notify_state(session): def get_notify_state(session):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id ' result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id '
@ -190,6 +225,21 @@ def get_notify_state(session):
return notify_states return notify_states
def get_notify_state_timeline(timeline):
monitor_db = database.MonitorDatabase()
result = monitor_db.select('SELECT on_created, agent_id '
'FROM notify_log '
'WHERE rating_key = ? '
'ORDER BY id DESC',
args=[timeline['rating_key']])
notify_states = []
for item in result:
notify_state = {'on_created': item[0],
'agent_id': item[1]}
notify_states.append(notify_state)
return notify_states
def set_notify_state(session, state, agent_info): def set_notify_state(session, state, agent_info):
@ -208,53 +258,77 @@ def set_notify_state(session, state, agent_info):
values = {'on_buffer': int(time.time())} values = {'on_buffer': int(time.time())}
elif state == 'watched': elif state == 'watched':
values = {'on_watched': int(time.time())} values = {'on_watched': int(time.time())}
elif state == 'created':
values = {'on_created': int(time.time())}
else: else:
return return
keys = {'session_key': session['session_key'], if state == 'created':
'rating_key': session['rating_key'], keys = {'rating_key': session['rating_key'],
'user_id': session['user_id'], 'agent_id': agent_info['id'],
'user': session['user'], 'agent_name': agent_info['name']}
'agent_id': agent_info['id'], else:
'agent_name': agent_info['name']} keys = {'session_key': session['session_key'],
'rating_key': session['rating_key'],
'user_id': session['user_id'],
'user': session['user'],
'agent_id': agent_info['id'],
'agent_name': agent_info['name']}
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values) monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
else: else:
logger.error('PlexPy Notifier :: Unable to set notify state.') logger.error('PlexPy Notifier :: Unable to set notify state.')
def build_notify_text(session, state): def build_notify_text(session=None, timeline=None, state=None):
from plexpy import pmsconnect, helpers
import re import re
# Get the server name # Get the server name
pms_connect = pmsconnect.PmsConnect() server_name = plexpy.CONFIG.PMS_NAME
server_name = pms_connect.get_server_pref(pref='FriendlyName')
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
if server_times:
updated_at = server_times[0]['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
else:
logger.error(u"PlexPy Notifier :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
# Get metadata feed for item # Get metadata feed for item
metadata = pms_connect.get_metadata_details(rating_key=session['rating_key']) if session:
rating_key = session['rating_key']
elif timeline:
rating_key = timeline['rating_key']
if metadata: pms_connect = pmsconnect.PmsConnect()
item_metadata = metadata['metadata'] metadata_list = pms_connect.get_metadata_details(rating_key=rating_key)
if metadata_list:
metadata = metadata_list['metadata']
else: else:
logger.error(u"PlexPy Notifier :: Unable to retrieve metadata for rating_key %s" % str(session['rating_key'])) logger.error(u"PlexPy Notifier :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
return [] return []
# Check for exclusion tags # Check for exclusion tags
if session['media_type'] == 'episode': if metadata['media_type'] == 'movie':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<movie>[^>]+.</movie>|<music>[^>]+.</music>', re.IGNORECASE)
elif session['media_type'] == 'movie':
# Regex pattern to remove the text in the tags we don't want # Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<tv>[^>]+.</tv>|<music>[^>]+.</music>', re.IGNORECASE) pattern = re.compile('<tv>[^>]+.</tv>|<music>[^>]+.</music>', re.IGNORECASE)
elif session['media_type'] == 'track': elif metadata['media_type'] == 'show' or metadata['media_type'] == 'episode':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<movie>[^>]+.</movie>|<music>[^>]+.</music>', re.IGNORECASE)
elif metadata['media_type'] == 'artist' or metadata['media_type'] == 'track':
# Regex pattern to remove the text in the tags we don't want # Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<tv>[^>]+.</tv>|<movie>[^>]+.</movie>', re.IGNORECASE) pattern = re.compile('<tv>[^>]+.</tv>|<movie>[^>]+.</movie>', re.IGNORECASE)
else: else:
pattern = None pattern = None
if session['media_type'] == 'episode' or session['media_type'] == 'movie' or session['media_type'] == 'track' \ if metadata['media_type'] == 'movie' \
and pattern: or metadata['media_type'] == 'show' or metadata['media_type'] == 'episode' \
or metadata['media_type'] == 'artist' or metadata['media_type'] == 'track' \
and pattern:
# Remove the unwanted tags and strip any unmatch tags too. # Remove the unwanted tags and strip any unmatch tags too.
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT)) on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT)) on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
@ -268,6 +342,8 @@ def build_notify_text(session, state):
on_buffer_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT)) on_buffer_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT))
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT)) on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT)) on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
on_created_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT))
on_created_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT))
else: else:
on_start_subject = plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT on_start_subject = plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT
on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT
@ -281,63 +357,86 @@ def build_notify_text(session, state):
on_buffer_body = plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT on_buffer_body = plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT
on_watched_subject = plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT on_watched_subject = plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT
on_watched_body = plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT on_watched_body = plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT
on_created_subject = plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT
on_created_body = plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT
# Create a title # Create a title
if session['media_type'] == 'episode': if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track':
full_title = '%s - %s' % (session['grandparent_title'], full_title = '%s - %s' % (metadata['grandparent_title'],
session['title']) metadata['title'])
elif session['media_type'] == 'track':
full_title = '%s - %s' % (session['grandparent_title'],
session['title'])
else: else:
full_title = session['title'] full_title = metadata['title']
# Generate a combined transcode decision value duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
if session['video_decision']:
if session['video_decision'] == 'transcode':
transcode_decision = 'Transcode'
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
transcode_decision = 'Direct Stream'
else:
transcode_decision = 'Direct Play'
else:
if session['audio_decision'] == 'transcode':
transcode_decision = 'Transcode'
else:
transcode_decision = 'Direct Play'
duration = helpers.convert_milliseconds_to_minutes(item_metadata['duration']) # Default values
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset']) transcode_decision = ''
stream_duration = 0 stream_duration = 0
if state != 'play': view_offset = 0
if session['paused_counter']: user = ''
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) - platform = ''
helpers.cast_to_float(session['paused_counter'])) / 60) player = ''
else: ip_address = 'N/A'
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60)
# Session values
if session:
# Generate a combined transcode decision value
if session['video_decision']:
if session['video_decision'] == 'transcode':
transcode_decision = 'Transcode'
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
transcode_decision = 'Direct Stream'
else:
transcode_decision = 'Direct Play'
elif session['audio_decision']:
if session['audio_decision'] == 'transcode':
transcode_decision = 'Transcode'
else:
transcode_decision = 'Direct Play'
if state != 'play':
if session['paused_counter']:
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) -
helpers.cast_to_float(session['paused_counter'])) / 60)
else:
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60)
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
user = session['friendly_name']
platform = session['platform']
player = session['player']
ip_address = session['ip_address'] if session['ip_address'] != '' else 'N/A'
progress_percent = helpers.get_percent(view_offset, duration) progress_percent = helpers.get_percent(view_offset, duration)
available_params = {'server_name': server_name, available_params = {'server_name': server_name,
'user': session['friendly_name'], 'server_uptime': server_uptime,
'platform': session['platform'], 'user': user,
'player': session['player'], 'platform': platform,
'media_type': session['media_type'], 'player': player,
'ip_address': ip_address,
'media_type': metadata['media_type'],
'title': full_title, 'title': full_title,
'show_name': item_metadata['grandparent_title'], 'show_name': metadata['grandparent_title'],
'episode_name': item_metadata['title'], 'episode_name': metadata['title'],
'artist_name': item_metadata['grandparent_title'], 'artist_name': metadata['grandparent_title'],
'album_name': item_metadata['parent_title'], 'album_name': metadata['parent_title'],
'season_num': item_metadata['parent_index'], 'track_name': metadata['title'],
'season_num00': item_metadata['parent_index'].zfill(2), 'season_num': metadata['parent_index'],
'episode_num': item_metadata['index'], 'season_num00': metadata['parent_index'].zfill(2),
'episode_num00': item_metadata['index'].zfill(2), 'episode_num': metadata['index'],
'episode_num00': metadata['index'].zfill(2),
'transcode_decision': transcode_decision, 'transcode_decision': transcode_decision,
'year': item_metadata['year'], 'year': metadata['year'],
'studio': item_metadata['studio'], 'studio': metadata['studio'],
'content_rating': item_metadata['content_rating'], 'content_rating': metadata['content_rating'],
'summary': item_metadata['summary'], 'directors': ', '.join(metadata['directors']),
'rating': item_metadata['rating'], 'writers': ', '.join(metadata['writers']),
'actors': ', '.join(metadata['actors']),
'genres': ', '.join(metadata['genres']),
'summary': metadata['summary'],
'tagline': metadata['tagline'],
'rating': metadata['rating'],
'duration': duration, 'duration': duration,
'stream_duration': stream_duration, 'stream_duration': stream_duration,
'remaining_duration': duration - view_offset, 'remaining_duration': duration - view_offset,
@ -489,12 +588,106 @@ def build_notify_text(session, state):
except: except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.") logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text]
else:
return [subject_text, body_text]
elif state == 'created':
# Default body text
body_text = '%s was recently added to Plex.' % full_title
if on_created_subject and on_created_body:
try:
subject_text = unicode(on_created_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = unicode(on_created_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text] return [subject_text, body_text]
else: else:
return [subject_text, body_text] return [subject_text, body_text]
else: else:
return None return None
def build_server_notify_text(state=None):
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
if server_times:
updated_at = server_times[0]['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
else:
logger.error(u"PlexPy Notifier :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
on_extdown_subject = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT
on_extdown_body = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT
on_intdown_subject = plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT
on_intdown_body = plexpy.CONFIG.NOTIFY_ON_INTDOWN_BODY_TEXT
available_params = {'server_name': server_name,
'server_uptime': server_uptime}
# Default text
subject_text = 'PlexPy (%s)' % server_name
if state == 'extdown':
# Default body text
body_text = 'The Plex Media Server remote access is down.'
if on_extdown_subject and on_extdown_body:
try:
subject_text = unicode(on_extdown_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = unicode(on_extdown_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text]
else:
return [subject_text, body_text]
elif state == 'intdown':
# Default body text
body_text = 'The Plex Media Server is down.'
if on_intdown_subject and on_intdown_body:
try:
subject_text = unicode(on_intdown_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = unicode(on_intdown_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text]
else:
return [subject_text, body_text]
else:
return None
def strip_tag(data): def strip_tag(data):
import re import re

View file

@ -50,7 +50,9 @@ AGENT_IDS = {"Growl": 0,
"OSX Notify": 8, "OSX Notify": 8,
"Boxcar2": 9, "Boxcar2": 9,
"Email": 10, "Email": 10,
"Twitter": 11} "Twitter": 11,
"IFTTT": 12,
"Telegram": 13}
def available_notification_agents(): def available_notification_agents():
agents = [{'name': 'Growl', agents = [{'name': 'Growl',
@ -63,7 +65,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.GROWL_ON_PAUSE, 'on_pause': plexpy.CONFIG.GROWL_ON_PAUSE,
'on_resume': plexpy.CONFIG.GROWL_ON_RESUME, 'on_resume': plexpy.CONFIG.GROWL_ON_RESUME,
'on_buffer': plexpy.CONFIG.GROWL_ON_BUFFER, 'on_buffer': plexpy.CONFIG.GROWL_ON_BUFFER,
'on_watched': plexpy.CONFIG.GROWL_ON_WATCHED 'on_watched': plexpy.CONFIG.GROWL_ON_WATCHED,
'on_created': plexpy.CONFIG.GROWL_ON_CREATED,
'on_extdown': plexpy.CONFIG.GROWL_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.GROWL_ON_INTDOWN
}, },
{'name': 'Prowl', {'name': 'Prowl',
'id': AGENT_IDS['Prowl'], 'id': AGENT_IDS['Prowl'],
@ -75,7 +80,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.PROWL_ON_PAUSE, 'on_pause': plexpy.CONFIG.PROWL_ON_PAUSE,
'on_resume': plexpy.CONFIG.PROWL_ON_RESUME, 'on_resume': plexpy.CONFIG.PROWL_ON_RESUME,
'on_buffer': plexpy.CONFIG.PROWL_ON_BUFFER, 'on_buffer': plexpy.CONFIG.PROWL_ON_BUFFER,
'on_watched': plexpy.CONFIG.PROWL_ON_WATCHED 'on_watched': plexpy.CONFIG.PROWL_ON_WATCHED,
'on_created': plexpy.CONFIG.PROWL_ON_CREATED,
'on_extdown': plexpy.CONFIG.PROWL_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PROWL_ON_INTDOWN
}, },
{'name': 'XBMC', {'name': 'XBMC',
'id': AGENT_IDS['XBMC'], 'id': AGENT_IDS['XBMC'],
@ -87,7 +95,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.XBMC_ON_PAUSE, 'on_pause': plexpy.CONFIG.XBMC_ON_PAUSE,
'on_resume': plexpy.CONFIG.XBMC_ON_RESUME, 'on_resume': plexpy.CONFIG.XBMC_ON_RESUME,
'on_buffer': plexpy.CONFIG.XBMC_ON_BUFFER, 'on_buffer': plexpy.CONFIG.XBMC_ON_BUFFER,
'on_watched': plexpy.CONFIG.XBMC_ON_WATCHED 'on_watched': plexpy.CONFIG.XBMC_ON_WATCHED,
'on_created': plexpy.CONFIG.XBMC_ON_CREATED,
'on_extdown': plexpy.CONFIG.XBMC_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.XBMC_ON_INTDOWN
}, },
{'name': 'Plex', {'name': 'Plex',
'id': AGENT_IDS['Plex'], 'id': AGENT_IDS['Plex'],
@ -99,7 +110,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.PLEX_ON_PAUSE, 'on_pause': plexpy.CONFIG.PLEX_ON_PAUSE,
'on_resume': plexpy.CONFIG.PLEX_ON_RESUME, 'on_resume': plexpy.CONFIG.PLEX_ON_RESUME,
'on_buffer': plexpy.CONFIG.PLEX_ON_BUFFER, 'on_buffer': plexpy.CONFIG.PLEX_ON_BUFFER,
'on_watched': plexpy.CONFIG.PLEX_ON_WATCHED 'on_watched': plexpy.CONFIG.PLEX_ON_WATCHED,
'on_created': plexpy.CONFIG.PLEX_ON_CREATED,
'on_extdown': plexpy.CONFIG.PLEX_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PLEX_ON_INTDOWN
}, },
{'name': 'NotifyMyAndroid', {'name': 'NotifyMyAndroid',
'id': AGENT_IDS['NMA'], 'id': AGENT_IDS['NMA'],
@ -111,7 +125,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.NMA_ON_PAUSE, 'on_pause': plexpy.CONFIG.NMA_ON_PAUSE,
'on_resume': plexpy.CONFIG.NMA_ON_RESUME, 'on_resume': plexpy.CONFIG.NMA_ON_RESUME,
'on_buffer': plexpy.CONFIG.NMA_ON_BUFFER, 'on_buffer': plexpy.CONFIG.NMA_ON_BUFFER,
'on_watched': plexpy.CONFIG.NMA_ON_WATCHED 'on_watched': plexpy.CONFIG.NMA_ON_WATCHED,
'on_created': plexpy.CONFIG.NMA_ON_CREATED,
'on_extdown': plexpy.CONFIG.NMA_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.NMA_ON_INTDOWN
}, },
{'name': 'Pushalot', {'name': 'Pushalot',
'id': AGENT_IDS['Pushalot'], 'id': AGENT_IDS['Pushalot'],
@ -123,7 +140,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.PUSHALOT_ON_PAUSE, 'on_pause': plexpy.CONFIG.PUSHALOT_ON_PAUSE,
'on_resume': plexpy.CONFIG.PUSHALOT_ON_RESUME, 'on_resume': plexpy.CONFIG.PUSHALOT_ON_RESUME,
'on_buffer': plexpy.CONFIG.PUSHALOT_ON_BUFFER, 'on_buffer': plexpy.CONFIG.PUSHALOT_ON_BUFFER,
'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED 'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED,
'on_created': plexpy.CONFIG.PUSHALOT_ON_CREATED,
'on_extdown': plexpy.CONFIG.PUSHALOT_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PUSHALOT_ON_INTDOWN
}, },
{'name': 'Pushbullet', {'name': 'Pushbullet',
'id': AGENT_IDS['Pushbullet'], 'id': AGENT_IDS['Pushbullet'],
@ -135,7 +155,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.PUSHBULLET_ON_PAUSE, 'on_pause': plexpy.CONFIG.PUSHBULLET_ON_PAUSE,
'on_resume': plexpy.CONFIG.PUSHBULLET_ON_RESUME, 'on_resume': plexpy.CONFIG.PUSHBULLET_ON_RESUME,
'on_buffer': plexpy.CONFIG.PUSHBULLET_ON_BUFFER, 'on_buffer': plexpy.CONFIG.PUSHBULLET_ON_BUFFER,
'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED 'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED,
'on_created': plexpy.CONFIG.PUSHBULLET_ON_CREATED,
'on_extdown': plexpy.CONFIG.PUSHBULLET_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PUSHBULLET_ON_INTDOWN
}, },
{'name': 'Pushover', {'name': 'Pushover',
'id': AGENT_IDS['Pushover'], 'id': AGENT_IDS['Pushover'],
@ -147,7 +170,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.PUSHOVER_ON_PAUSE, 'on_pause': plexpy.CONFIG.PUSHOVER_ON_PAUSE,
'on_resume': plexpy.CONFIG.PUSHOVER_ON_RESUME, 'on_resume': plexpy.CONFIG.PUSHOVER_ON_RESUME,
'on_buffer': plexpy.CONFIG.PUSHOVER_ON_BUFFER, 'on_buffer': plexpy.CONFIG.PUSHOVER_ON_BUFFER,
'on_watched': plexpy.CONFIG.PUSHOVER_ON_WATCHED 'on_watched': plexpy.CONFIG.PUSHOVER_ON_WATCHED,
'on_created': plexpy.CONFIG.PUSHOVER_ON_CREATED,
'on_extdown': plexpy.CONFIG.PUSHOVER_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PUSHOVER_ON_INTDOWN
}, },
{'name': 'Boxcar2', {'name': 'Boxcar2',
'id': AGENT_IDS['Boxcar2'], 'id': AGENT_IDS['Boxcar2'],
@ -159,7 +185,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.BOXCAR_ON_PAUSE, 'on_pause': plexpy.CONFIG.BOXCAR_ON_PAUSE,
'on_resume': plexpy.CONFIG.BOXCAR_ON_RESUME, 'on_resume': plexpy.CONFIG.BOXCAR_ON_RESUME,
'on_buffer': plexpy.CONFIG.BOXCAR_ON_BUFFER, 'on_buffer': plexpy.CONFIG.BOXCAR_ON_BUFFER,
'on_watched': plexpy.CONFIG.BOXCAR_ON_WATCHED 'on_watched': plexpy.CONFIG.BOXCAR_ON_WATCHED,
'on_created': plexpy.CONFIG.BOXCAR_ON_CREATED,
'on_extdown': plexpy.CONFIG.BOXCAR_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.BOXCAR_ON_INTDOWN
}, },
{'name': 'E-mail', {'name': 'E-mail',
'id': AGENT_IDS['Email'], 'id': AGENT_IDS['Email'],
@ -171,7 +200,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.EMAIL_ON_PAUSE, 'on_pause': plexpy.CONFIG.EMAIL_ON_PAUSE,
'on_resume': plexpy.CONFIG.EMAIL_ON_RESUME, 'on_resume': plexpy.CONFIG.EMAIL_ON_RESUME,
'on_buffer': plexpy.CONFIG.EMAIL_ON_BUFFER, 'on_buffer': plexpy.CONFIG.EMAIL_ON_BUFFER,
'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED 'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED,
'on_created': plexpy.CONFIG.EMAIL_ON_CREATED,
'on_extdown': plexpy.CONFIG.EMAIL_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.EMAIL_ON_INTDOWN
}, },
{'name': 'Twitter', {'name': 'Twitter',
'id': AGENT_IDS['Twitter'], 'id': AGENT_IDS['Twitter'],
@ -183,7 +215,40 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.TWITTER_ON_PAUSE, 'on_pause': plexpy.CONFIG.TWITTER_ON_PAUSE,
'on_resume': plexpy.CONFIG.TWITTER_ON_RESUME, 'on_resume': plexpy.CONFIG.TWITTER_ON_RESUME,
'on_buffer': plexpy.CONFIG.TWITTER_ON_BUFFER, 'on_buffer': plexpy.CONFIG.TWITTER_ON_BUFFER,
'on_watched': plexpy.CONFIG.TWITTER_ON_WATCHED 'on_watched': plexpy.CONFIG.TWITTER_ON_WATCHED,
'on_created': plexpy.CONFIG.TWITTER_ON_CREATED,
'on_extdown': plexpy.CONFIG.TWITTER_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.TWITTER_ON_INTDOWN
},
{'name': 'IFTTT',
'id': AGENT_IDS['IFTTT'],
'config_prefix': 'ifttt',
'has_config': True,
'state': checked(plexpy.CONFIG.IFTTT_ENABLED),
'on_play': plexpy.CONFIG.IFTTT_ON_PLAY,
'on_stop': plexpy.CONFIG.IFTTT_ON_STOP,
'on_pause': plexpy.CONFIG.IFTTT_ON_PAUSE,
'on_resume': plexpy.CONFIG.IFTTT_ON_RESUME,
'on_buffer': plexpy.CONFIG.IFTTT_ON_BUFFER,
'on_watched': plexpy.CONFIG.IFTTT_ON_WATCHED,
'on_created': plexpy.CONFIG.IFTTT_ON_CREATED,
'on_extdown': plexpy.CONFIG.IFTTT_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.IFTTT_ON_INTDOWN
},
{'name': 'Telegram',
'id': AGENT_IDS['Telegram'],
'config_prefix': 'telegram',
'has_config': True,
'state': checked(plexpy.CONFIG.TELEGRAM_ENABLED),
'on_play': plexpy.CONFIG.TELEGRAM_ON_PLAY,
'on_stop': plexpy.CONFIG.TELEGRAM_ON_STOP,
'on_pause': plexpy.CONFIG.TELEGRAM_ON_PAUSE,
'on_resume': plexpy.CONFIG.TELEGRAM_ON_RESUME,
'on_buffer': plexpy.CONFIG.TELEGRAM_ON_BUFFER,
'on_watched': plexpy.CONFIG.TELEGRAM_ON_WATCHED,
'on_created': plexpy.CONFIG.TELEGRAM_ON_CREATED,
'on_extdown': plexpy.CONFIG.TELEGRAM_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.TELEGRAM_ON_INTDOWN
} }
] ]
@ -200,7 +265,10 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.OSX_NOTIFY_ON_PAUSE, 'on_pause': plexpy.CONFIG.OSX_NOTIFY_ON_PAUSE,
'on_resume': plexpy.CONFIG.OSX_NOTIFY_ON_RESUME, 'on_resume': plexpy.CONFIG.OSX_NOTIFY_ON_RESUME,
'on_buffer': plexpy.CONFIG.OSX_NOTIFY_ON_BUFFER, 'on_buffer': plexpy.CONFIG.OSX_NOTIFY_ON_BUFFER,
'on_watched': plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED 'on_watched': plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED,
'on_created': plexpy.CONFIG.OSX_NOTIFY_ON_CREATED,
'on_extdown': plexpy.CONFIG.OSX_NOTIFY_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.OSX_NOTIFY_ON_INTDOWN
}) })
return agents return agents
@ -245,6 +313,12 @@ def get_notification_agent_config(config_id):
elif config_id == 11: elif config_id == 11:
tweet = TwitterNotifier() tweet = TwitterNotifier()
return tweet.return_config_options() return tweet.return_config_options()
elif config_id == 12:
iftttClient = IFTTT()
return iftttClient.return_config_options()
elif config_id == 13:
telegramClient = TELEGRAM()
return telegramClient.return_config_options()
else: else:
return [] return []
else: else:
@ -290,6 +364,12 @@ def send_notification(config_id, subject, body):
elif config_id == 11: elif config_id == 11:
tweet = TwitterNotifier() tweet = TwitterNotifier()
tweet.notify(subject=subject, message=body) tweet.notify(subject=subject, message=body)
elif config_id == 12:
iftttClient = IFTTT()
iftttClient.notify(subject=subject, message=body)
elif config_id == 13:
telegramClient = TELEGRAM()
telegramClient.notify(message=body, event=subject)
else: else:
logger.debug(u"PlexPy Notifier :: Unknown agent id received.") logger.debug(u"PlexPy Notifier :: Unknown agent id received.")
else: else:
@ -868,7 +948,7 @@ class PUSHOVER(object):
data = {'token': self.application_token, data = {'token': self.application_token,
'user': plexpy.CONFIG.PUSHOVER_KEYS, 'user': plexpy.CONFIG.PUSHOVER_KEYS,
'title': event, 'title': event.encode("utf-8"),
'message': message.encode("utf-8"), 'message': message.encode("utf-8"),
'sound': plexpy.CONFIG.PUSHOVER_SOUND, 'sound': plexpy.CONFIG.PUSHOVER_SOUND,
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY} 'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
@ -924,10 +1004,10 @@ class PUSHOVER(object):
return {'': ''} return {'': ''}
def return_config_options(self): def return_config_options(self):
config_option = [{'label': 'Pushover User Key', config_option = [{'label': 'Pushover User or Group Key',
'value': self.keys, 'value': self.keys,
'name': 'pushover_keys', 'name': 'pushover_keys',
'description': 'Your Pushover user key.', 'description': 'Your Pushover user or group key.',
'input_type': 'text' 'input_type': 'text'
}, },
{'label': 'Priority', {'label': 'Priority',
@ -1268,6 +1348,8 @@ class Email(object):
mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string()) mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string())
mailserver.quit() mailserver.quit()
logger.info(u"Email notifications sent.")
return True return True
except Exception, e: except Exception, e:
@ -1319,4 +1401,134 @@ class Email(object):
} }
] ]
return config_option return config_option
class IFTTT(object):
def __init__(self):
self.apikey = plexpy.CONFIG.IFTTT_KEY
self.event = plexpy.CONFIG.IFTTT_EVENT
def notify(self, message, subject):
if not message or not subject:
return
http_handler = HTTPSConnection("maker.ifttt.com")
data = {'value1': subject.encode("utf-8"),
'value2': message.encode("utf-8")}
# logger.debug("Ifttt SENDING: %s" % json.dumps(data))
http_handler.request("POST",
"/trigger/%s/with/key/%s" % (self.event, self.apikey),
headers={'Content-type': "application/json"},
body=json.dumps(data))
response = http_handler.getresponse()
request_status = response.status
# logger.debug(u"Ifttt response status: %r" % request_status)
# logger.debug(u"Ifttt response headers: %r" % response.getheaders())
# logger.debug(u"Ifttt response body: %r" % response.read())
if request_status == 200:
logger.info(u"Ifttt notifications sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.info(u"Ifttt request failed: %s" % response.reason)
return False
else:
logger.info(u"Ifttt notification failed serverside.")
return False
def test(self):
return self.notify('PlexPy', 'Test Message')
def return_config_options(self):
config_option = [{'label': 'Ifttt Maker Channel Key',
'value': self.apikey,
'name': 'ifttt_key',
'description': 'Your Ifttt key. You can get a key from <a href="https://ifttt.com/maker" target="_blank">here</a>.',
'input_type': 'text'
},
{'label': 'Ifttt Event',
'value': self.event,
'name': 'ifttt_event',
'description': 'The Ifttt maker event to fire. The notification subject and body will be sent'
' as value1 and value2 respectively.',
'input_type': 'text'
},
{'label': 'Test Event',
'value': 'Test Event',
'name': 'testIFTTT',
'description': 'Test if IFTTT notifications are working. See logs for troubleshooting.',
'input_type': 'button'
}
]
return config_option
class TELEGRAM(object):
def __init__(self):
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
def conf(self, options):
return cherrypy.config['config'].get('Telegram', options)
def notify(self, message, event):
if not message or not event:
return
http_handler = HTTPSConnection("api.telegram.org")
data = {'chat_id': self.chat_id,
'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
http_handler.request("POST",
"/bot%s/%s" % (self.bot_token, "sendMessage"),
headers={'Content-type': "application/x-www-form-urlencoded"},
body=urlencode(data))
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
logger.info(u"Telegram notifications sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.info(u"Telegram request failed: %s" % response.reason)
return False
else:
logger.info(u"Telegram notification failed serverside.")
return False
def updateLibrary(self):
#For uniformity reasons not removed
return
def test(self, bot_token, chat_id):
self.enabled = True
self.bot_token = bot_token
self.chat_id = chat_id
self.notify('Main Screen Activate', 'Test Message')
def return_config_options(self):
config_option = [{'label': 'Telegram Bot Token',
'value': self.bot_token,
'name': 'telegram_bot_token',
'description': 'Your Telegram bot token. Contact <a href="http://telegram.me/BotFather" target="_blank">@BotFather</a> on Telegram to get one.',
'input_type': 'text'
},
{'label': 'Telegram Chat ID',
'value': self.chat_id,
'name': 'telegram_chat_id',
'description': 'Your Telegram Chat ID or Group ID. Contact <a href="http://telegram.me/myidbot" target="_blank">@myidbot</a> on Telegram to get an ID.',
'input_type': 'text'
}
]
return config_option

View file

@ -444,3 +444,22 @@ class PlexTV(object):
return clean_servers return clean_servers
return json.dumps(clean_servers, indent=4) return json.dumps(clean_servers, indent=4)
def get_server_times(self):
servers = self.get_plextv_server_list(output_format='xml')
server_times = []
try:
xml_head = servers.getElementsByTagName('Server')
except:
logger.warn("Error parsing XML for Plex servers.")
return []
for a in xml_head:
if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER:
server_times.append({"created_at": helpers.get_xml_attr(a, 'createdAt'),
"updated_at": helpers.get_xml_attr(a, 'updatedAt')
})
break
return server_times

View file

@ -1,4 +1,4 @@
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -247,6 +247,8 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
logger.debug(u"PlexPy Importer :: Disabling monitoring while import in progress.") logger.debug(u"PlexPy Importer :: Disabling monitoring while import in progress.")
plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions', plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
hours=0, minutes=0, seconds=0) hours=0, minutes=0, seconds=0)
plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
hours=0, minutes=0, seconds=0)
ap = activity_processor.ActivityProcessor() ap = activity_processor.ActivityProcessor()
user_data = users.Users() user_data = users.Users()

View file

@ -13,12 +13,29 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers, users, http_handler from plexpy import logger, helpers, users, http_handler, common
from urlparse import urlparse from urlparse import urlparse
import plexpy import plexpy
import urllib2 import urllib2
def get_server_friendly_name():
logger.info("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()
for server in servers_info:
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()
return server_name
class PmsConnect(object): class PmsConnect(object):
""" """
@ -254,6 +271,36 @@ class PmsConnect(object):
return request return request
""" """
Return account details.
Optional parameters: output_format { dict, json }
Output: array
"""
def get_account(self, output_format=''):
uri = '/myplex/account'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
return request
"""
Refresh Plex remote access port mapping.
Optional parameters: None
Output: None
"""
def put_refresh_reachability(self):
uri = '/myplex/refreshReachability'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='PUT')
return request
"""
Return processed and validated list of recently added items. Return processed and validated list of recently added items.
Parameters required: count { number of results to return } Parameters required: count { number of results to return }
@ -281,10 +328,13 @@ class PmsConnect(object):
recents_main = a.getElementsByTagName('Directory') recents_main = a.getElementsByTagName('Directory')
for item in recents_main: for item in recents_main:
recent_type = helpers.get_xml_attr(item, 'type') recent_type = helpers.get_xml_attr(item, 'type')
recent_items = {'type': recent_type, recent_items = {'media_type': recent_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'), 'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
'title': helpers.get_xml_attr(item, 'title'), 'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'), 'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'library_id': helpers.get_xml_attr(item, 'librarySectionID'),
'library_title': helpers.get_xml_attr(item, 'librarySectionTitle'),
'thumb': helpers.get_xml_attr(item, 'thumb'), 'thumb': helpers.get_xml_attr(item, 'thumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt') 'added_at': helpers.get_xml_attr(item, 'addedAt')
} }
@ -296,10 +346,12 @@ class PmsConnect(object):
recent_type = helpers.get_xml_attr(item, 'type') recent_type = helpers.get_xml_attr(item, 'type')
if recent_type == 'movie': if recent_type == 'movie':
recent_items = {'type': recent_type, recent_items = {'media_type': recent_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'), 'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'title': helpers.get_xml_attr(item, 'title'), 'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'), 'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'library_id': helpers.get_xml_attr(item, 'librarySectionID'),
'library_title': helpers.get_xml_attr(item, 'librarySectionTitle'),
'year': helpers.get_xml_attr(item, 'year'), 'year': helpers.get_xml_attr(item, 'year'),
'thumb': helpers.get_xml_attr(item, 'thumb'), 'thumb': helpers.get_xml_attr(item, 'thumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt') 'added_at': helpers.get_xml_attr(item, 'addedAt')
@ -369,7 +421,7 @@ class PmsConnect(object):
directors.append(helpers.get_xml_attr(director, 'tag')) directors.append(helpers.get_xml_attr(director, 'tag'))
if metadata_type == 'show': if metadata_type == 'show':
metadata = {'type': metadata_type, metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@ -401,16 +453,16 @@ class PmsConnect(object):
elif metadata_type == 'season': elif metadata_type == 'season':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey') parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
show_details = self.get_metadata_details(parent_rating_key) show_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type, metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'), 'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'index': helpers.get_xml_attr(metadata_main, 'index'), 'index': helpers.get_xml_attr(metadata_main, 'index'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'), 'studio': show_details['metadata']['studio'],
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'), 'content_rating': show_details['metadata']['content_rating'],
'summary': show_details['metadata']['summary'], 'summary': show_details['metadata']['summary'],
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'), 'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'), 'rating': helpers.get_xml_attr(metadata_main, 'rating'),
@ -425,14 +477,16 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': genres, 'genres': show_details['metadata']['genres'],
'actors': actors, 'actors': show_details['metadata']['actors'],
'writers': writers, 'writers': show_details['metadata']['writers'],
'directors': directors 'directors': show_details['metadata']['directors']
} }
metadata_list = {'metadata': metadata} metadata_list = {'metadata': metadata}
elif metadata_type == 'episode': elif metadata_type == 'episode':
metadata = {'type': metadata_type, grandparent_rating_key = helpers.get_xml_attr(metadata_main, 'grandparentRatingKey')
show_details = self.get_metadata_details(grandparent_rating_key)
metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'), 'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
@ -440,7 +494,7 @@ class PmsConnect(object):
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'), 'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'index': helpers.get_xml_attr(metadata_main, 'index'), 'index': helpers.get_xml_attr(metadata_main, 'index'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'), 'studio': show_details['metadata']['studio'],
'title': helpers.get_xml_attr(metadata_main, 'title'), 'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'), 'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'), 'summary': helpers.get_xml_attr(metadata_main, 'summary'),
@ -457,14 +511,14 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': show_details['metadata']['genres'],
'actors': show_details['metadata']['actors'],
'writers': writers, 'writers': writers,
'directors': directors, 'directors': directors
'genres': genres,
'actors': actors
} }
metadata_list = {'metadata': metadata} metadata_list = {'metadata': metadata}
elif metadata_type == 'movie': elif metadata_type == 'movie':
metadata = {'type': metadata_type, metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@ -494,7 +548,7 @@ class PmsConnect(object):
} }
metadata_list = {'metadata': metadata} metadata_list = {'metadata': metadata}
elif metadata_type == 'artist': elif metadata_type == 'artist':
metadata = {'type': metadata_type, metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'), 'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@ -526,7 +580,7 @@ class PmsConnect(object):
elif metadata_type == 'album': elif metadata_type == 'album':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey') parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
artist_details = self.get_metadata_details(parent_rating_key) artist_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type, metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'), 'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'), 'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
@ -559,7 +613,7 @@ class PmsConnect(object):
elif metadata_type == 'track': elif metadata_type == 'track':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey') parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
album_details = self.get_metadata_details(parent_rating_key) album_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type, metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'), 'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'), 'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'), 'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
@ -584,7 +638,7 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'), 'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'), 'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'), 'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': genres, 'genres': album_details['metadata']['genres'],
'actors': actors, 'actors': actors,
'writers': writers, 'writers': writers,
'directors': directors 'directors': directors
@ -595,6 +649,49 @@ class PmsConnect(object):
return metadata_list return metadata_list
"""
Return processed and validated metadata list for all children of requested item.
Parameters required: rating_key { Plex ratingKey }
Output: array
"""
def get_metadata_children_details(self, rating_key=''):
metadata = self.get_metadata_children(str(rating_key), output_format='xml')
try:
xml_head = metadata.getElementsByTagName('MediaContainer')
except:
logger.warn("Unable to parse XML for get_metadata_children.")
return []
metadata_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
metadata_list = {'metadata': None}
return metadata_list
if a.getElementsByTagName('Video'):
metadata_main = a.getElementsByTagName('Video')
for item in metadata_main:
child_rating_key = helpers.get_xml_attr(item, 'ratingKey')
metadata = self.get_metadata_details(str(child_rating_key))
if metadata:
metadata_list.append(metadata['metadata'])
elif a.getElementsByTagName('Track'):
metadata_main = a.getElementsByTagName('Track')
for item in metadata_main:
child_rating_key = helpers.get_xml_attr(item, 'ratingKey')
metadata = self.get_metadata_details(str(child_rating_key))
if metadata:
metadata_list.append(metadata['metadata'])
output = {'metadata': metadata_list}
return output
""" """
Return processed and validated session list. Return processed and validated session list.
@ -706,6 +803,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'], 'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'], 'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'], 'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address'),
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id, 'machine_id': machine_id,
@ -826,6 +924,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'], 'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'], 'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'], 'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address'),
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id, 'machine_id': machine_id,
@ -882,6 +981,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'], 'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'], 'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'], 'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address'),
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id, 'machine_id': machine_id,
@ -938,6 +1038,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'], 'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'], 'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'], 'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address'),
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id, 'machine_id': machine_id,
@ -1027,6 +1128,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'], 'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'], 'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'], 'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address'),
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'), 'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'), 'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id, 'machine_id': machine_id,
@ -1071,11 +1173,8 @@ class PmsConnect(object):
logger.warn(u"No known stream types found in session list.") logger.warn(u"No known stream types found in session list.")
# Rename Mystery platform names # Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3', session_output['platform'] = common.PLATFORM_NAME_OVERRIDES.get(session_output['platform'],
'Mystery 4': 'Playstation 4', session_output['platform'])
'Mystery 5': 'Xbox 360'}
session_output['platform'] = platform_names.get(session_output['platform'],
session_output['platform'])
return session_output return session_output
@ -1432,9 +1531,9 @@ class PmsConnect(object):
for result in result_data: for result in result_data:
rating_key = helpers.get_xml_attr(result, 'ratingKey') rating_key = helpers.get_xml_attr(result, 'ratingKey')
metadata = self.get_metadata_details(rating_key=rating_key) metadata = self.get_metadata_details(rating_key=rating_key)
if metadata['metadata']['type'] == 'movie': if metadata['metadata']['media_type'] == 'movie':
search_results_list['movie'].append(metadata['metadata']) search_results_list['movie'].append(metadata['metadata'])
elif metadata['metadata']['type'] == 'episode': elif metadata['metadata']['media_type'] == 'episode':
search_results_list['episode'].append(metadata['metadata']) search_results_list['episode'].append(metadata['metadata'])
search_results_count += 1 search_results_count += 1
@ -1443,7 +1542,7 @@ class PmsConnect(object):
for result in result_data: for result in result_data:
rating_key = helpers.get_xml_attr(result, 'ratingKey') rating_key = helpers.get_xml_attr(result, 'ratingKey')
metadata = self.get_metadata_details(rating_key=rating_key) metadata = self.get_metadata_details(rating_key=rating_key)
if metadata['metadata']['type'] == 'show': if metadata['metadata']['media_type'] == 'show':
search_results_list['show'].append(metadata['metadata']) search_results_list['show'].append(metadata['metadata'])
show_seasons = self.get_item_children(rating_key=metadata['metadata']['rating_key']) show_seasons = self.get_item_children(rating_key=metadata['metadata']['rating_key'])
@ -1455,9 +1554,9 @@ class PmsConnect(object):
search_results_list['season'].append(metadata['metadata']) search_results_list['season'].append(metadata['metadata'])
search_results_count += 1 search_results_count += 1
elif metadata['metadata']['type'] == 'artist': elif metadata['metadata']['media_type'] == 'artist':
search_results_list['artist'].append(metadata['metadata']) search_results_list['artist'].append(metadata['metadata'])
elif metadata['metadata']['type'] == 'album': elif metadata['metadata']['media_type'] == 'album':
search_results_list['album'].append(metadata['metadata']) search_results_list['album'].append(metadata['metadata'])
search_results_count += 1 search_results_count += 1
@ -1579,4 +1678,26 @@ class PmsConnect(object):
'children': parents} 'children': parents}
} }
return key_list 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:
logger.warn("Unable to parse XML for get_server_response.")
return None
server_response = {}
for a in xml_head:
server_response = {'mapping_state': helpers.get_xml_attr(a, 'mappingState'),
'mapping_error': helpers.get_xml_attr(a, 'mappingError'),
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
'public_port': helpers.get_xml_attr(a, 'publicPort')
}
return server_response

View file

@ -85,10 +85,7 @@ class Users(object):
user_thumb = item['user_thumb'] user_thumb = item['user_thumb']
# Rename Mystery platform names # Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3', platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
row = {"id": item['id'], row = {"id": item['id'],
"plays": item['plays'], "plays": item['plays'],
@ -179,10 +176,7 @@ class Users(object):
thumb = item["thumb"] thumb = item["thumb"]
# Rename Mystery platform names # Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3', platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
row = {"id": item['id'], row = {"id": item['id'],
"last_seen": item['last_seen'], "last_seen": item['last_seen'],
@ -533,10 +527,7 @@ class Users(object):
for item in result: for item in result:
# Rename Mystery platform names # Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3', platform_type = common.PLATFORM_NAME_OVERRIDES.get(item[2], item[2])
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform_type = platform_names.get(item[2], item[2])
row = {'player_name': item[0], row = {'player_name': item[0],
'platform_type': platform_type, 'platform_type': platform_type,

View file

@ -1,2 +1,2 @@
PLEXPY_VERSION = "master" PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.2.3" PLEXPY_RELEASE_VERSION = "1.2.4"

View file

@ -1,4 +1,4 @@
# This file is part of PlexPy. # This file is part of PlexPy.
# #
# PlexPy is free software: you can redistribute it and/or modify # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@ -139,4 +139,14 @@ def process(opcode, data):
activity = activity_handler.ActivityHandler(timeline=time_line[0]) activity = activity_handler.ActivityHandler(timeline=time_line[0])
activity.process() activity.process()
#if type == 'timeline':
# try:
# time_line = info.get('_children')
# except:
# logger.debug(u"PlexPy WebSocket :: Timeline event found but unable to get timeline data.")
# return False
# activity = activity_handler.TimelineHandler(timeline=time_line[0])
# activity.process()
return True return True

View file

@ -46,9 +46,11 @@ def serve_template(templatename, **kwargs):
_hplookup = TemplateLookup(directories=[template_dir]) _hplookup = TemplateLookup(directories=[template_dir])
server_name = plexpy.CONFIG.PMS_NAME
try: try:
template = _hplookup.get_template(templatename) template = _hplookup.get_template(templatename)
return template.render(**kwargs) return template.render(server_name=server_name, **kwargs)
except: except:
return exceptions.html_error_template().render() return exceptions.html_error_template().render()
@ -71,7 +73,8 @@ class WebInterface(object):
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS, "home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS,
"home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS, "home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS,
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
"pms_name": plexpy.CONFIG.PMS_NAME
} }
return serve_template(templatename="index.html", title="Home", config=config) return serve_template(templatename="index.html", title="Home", config=config)
@ -87,13 +90,14 @@ class WebInterface(object):
"pms_token": plexpy.CONFIG.PMS_TOKEN, "pms_token": plexpy.CONFIG.PMS_TOKEN,
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL), "pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
"pms_uuid": plexpy.CONFIG.PMS_UUID, "pms_uuid": plexpy.CONFIG.PMS_UUID,
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE), "movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE), "music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START), "movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
"music_notify_on_start": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_START), "music_notify_on_start": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_START),
"video_logging_enable": checked(plexpy.CONFIG.VIDEO_LOGGING_ENABLE), "movie_logging_enable": checked(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
"tv_logging_enable": checked(plexpy.CONFIG.TV_LOGGING_ENABLE),
"music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE), "music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL, "logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"check_github": checked(plexpy.CONFIG.CHECK_GITHUB) "check_github": checked(plexpy.CONFIG.CHECK_GITHUB)
@ -420,8 +424,8 @@ class WebInterface(object):
"grouping_global_history": checked(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY), "grouping_global_history": checked(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
"grouping_user_history": checked(plexpy.CONFIG.GROUPING_USER_HISTORY), "grouping_user_history": checked(plexpy.CONFIG.GROUPING_USER_HISTORY),
"grouping_charts": checked(plexpy.CONFIG.GROUPING_CHARTS), "grouping_charts": checked(plexpy.CONFIG.GROUPING_CHARTS),
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE), "movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE), "music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START), "tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START), "movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
@ -432,16 +436,20 @@ class WebInterface(object):
"tv_notify_on_pause": checked(plexpy.CONFIG.TV_NOTIFY_ON_PAUSE), "tv_notify_on_pause": checked(plexpy.CONFIG.TV_NOTIFY_ON_PAUSE),
"movie_notify_on_pause": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_PAUSE), "movie_notify_on_pause": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_PAUSE),
"music_notify_on_pause": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_PAUSE), "music_notify_on_pause": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_PAUSE),
"monitor_remote_access": checked(plexpy.CONFIG.MONITOR_REMOTE_ACCESS),
"monitoring_interval": plexpy.CONFIG.MONITORING_INTERVAL, "monitoring_interval": plexpy.CONFIG.MONITORING_INTERVAL,
"monitoring_use_websocket": checked(plexpy.CONFIG.MONITORING_USE_WEBSOCKET), "monitoring_use_websocket": checked(plexpy.CONFIG.MONITORING_USE_WEBSOCKET),
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL, "refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
"refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP), "refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
"ip_logging_enable": checked(plexpy.CONFIG.IP_LOGGING_ENABLE), "ip_logging_enable": checked(plexpy.CONFIG.IP_LOGGING_ENABLE),
"video_logging_enable": checked(plexpy.CONFIG.VIDEO_LOGGING_ENABLE), "movie_logging_enable": checked(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
"tv_logging_enable": checked(plexpy.CONFIG.TV_LOGGING_ENABLE),
"music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE), "music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL, "logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE), "pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
"notify_consecutive": checked(plexpy.CONFIG.NOTIFY_CONSECUTIVE), "notify_consecutive": checked(plexpy.CONFIG.NOTIFY_CONSECUTIVE),
"notify_recently_added_grandparent": checked(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT),
"notify_recently_added_delay": plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY,
"notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT, "notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT,
"notify_on_start_subject_text": plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT, "notify_on_start_subject_text": plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT,
"notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT, "notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT,
@ -455,6 +463,12 @@ class WebInterface(object):
"notify_on_buffer_body_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT, "notify_on_buffer_body_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT,
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT, "notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT, "notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
"notify_on_created_subject_text": plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT,
"notify_on_created_body_text": plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT,
"notify_on_extdown_subject_text": plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT,
"notify_on_extdown_body_text": plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT,
"notify_on_intdown_subject_text": plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT,
"notify_on_intdown_body_text": plexpy.CONFIG.NOTIFY_ON_INTDOWN_BODY_TEXT,
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE), "home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE),
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
@ -474,12 +488,13 @@ class WebInterface(object):
checked_configs = [ checked_configs = [
"launch_browser", "enable_https", "api_enabled", "freeze_db", "check_github", "launch_browser", "enable_https", "api_enabled", "freeze_db", "check_github",
"grouping_global_history", "grouping_user_history", "grouping_charts", "pms_use_bif", "pms_ssl", "grouping_global_history", "grouping_user_history", "grouping_charts", "pms_use_bif", "pms_ssl",
"tv_notify_enable", "movie_notify_enable", "music_notify_enable", "monitoring_use_websocket", "movie_notify_enable", "tv_notify_enable", "music_notify_enable", "monitoring_use_websocket",
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start", "tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start",
"tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop", "tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop",
"tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup", "tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup",
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type", "ip_logging_enable", "movie_logging_enable", "tv_logging_enable", "music_logging_enable",
"group_history_tables", "notify_consecutive" "pms_is_remote", "home_stats_type", "group_history_tables", "notify_consecutive",
"notify_recently_added_grandparent", "monitor_remote_access"
] ]
for checked_config in checked_configs: for checked_config in checked_configs:
if checked_config not in kwargs: if checked_config not in kwargs:
@ -571,27 +586,28 @@ class WebInterface(object):
custom_where = [] custom_where = []
if user_id: if user_id:
custom_where = [['session_history.user_id', user_id]] custom_where.append(['session_history.user_id', user_id])
elif user: elif user:
custom_where = [['session_history.user', user]] custom_where.append(['session_history.user', user])
if 'rating_key' in kwargs: if 'rating_key' in kwargs:
rating_key = kwargs.get('rating_key', "") rating_key = kwargs.get('rating_key', "")
custom_where = [['session_history.rating_key', rating_key]] custom_where.append(['session_history.rating_key', rating_key])
if 'parent_rating_key' in kwargs: if 'parent_rating_key' in kwargs:
rating_key = kwargs.get('parent_rating_key', "") rating_key = kwargs.get('parent_rating_key', "")
custom_where = [['session_history.parent_rating_key', rating_key]] custom_where.append(['session_history.parent_rating_key', rating_key])
if 'grandparent_rating_key' in kwargs: if 'grandparent_rating_key' in kwargs:
rating_key = kwargs.get('grandparent_rating_key', "") rating_key = kwargs.get('grandparent_rating_key', "")
custom_where = [['session_history.grandparent_rating_key', rating_key]] custom_where.append(['session_history.grandparent_rating_key', rating_key])
if 'start_date' in kwargs: if 'start_date' in kwargs:
start_date = kwargs.get('start_date', "") start_date = kwargs.get('start_date', "")
custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]] custom_where.append(['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date])
if 'reference_id' in kwargs: if 'reference_id' in kwargs:
reference_id = kwargs.get('reference_id', "") reference_id = kwargs.get('reference_id', "")
custom_where = [['session_history.reference_id', reference_id]] custom_where.append(['session_history.reference_id', reference_id])
if 'media_type' in kwargs: if 'media_type' in kwargs:
media_type = kwargs.get('media_type', "") media_type = kwargs.get('media_type', "")
custom_where = [['session_history_metadata.media_type', media_type]] if media_type != 'all':
custom_where.append(['session_history_metadata.media_type', media_type])
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping, watched_percent=watched_percent) history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping, watched_percent=watched_percent)
@ -652,6 +668,16 @@ class WebInterface(object):
else: else:
return "Error sending tweet" return "Error sending tweet"
@cherrypy.expose
def test_ifttt(self):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
event = notifiers.IFTTT()
result = event.test()
if result:
return "Notification successful."
else:
return "Error sending event."
@cherrypy.expose @cherrypy.expose
def osxnotifyregister(self, app): def osxnotifyregister(self, app):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store" cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
@ -697,6 +723,12 @@ class WebInterface(object):
try: try:
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_current_activity() result = pms_connect.get_current_activity()
data_factory = datafactory.DataFactory()
for session in result['sessions']:
if not session['ip_address']:
ip_address = data_factory.get_session_ip(session['session_key'])
session['ip_address'] = ip_address
except: except:
return serve_template(templatename="current_activity.html", data=None) return serve_template(templatename="current_activity.html", data=None)
@ -777,11 +809,11 @@ class WebInterface(object):
data_factory = datafactory.DataFactory() data_factory = datafactory.DataFactory()
metadata = data_factory.get_metadata_details(row_id=item_id) metadata = data_factory.get_metadata_details(row_id=item_id)
elif item_id == 'movie': elif item_id == 'movie':
metadata = {'type': 'library', 'library': 'movie', 'media_type': 'movie', 'title': 'Movies'} metadata = {'media_type': 'library', 'library': 'movie', 'media_type_filter': 'movie', 'title': 'Movies'}
elif item_id == 'show': elif item_id == 'show':
metadata = {'type': 'library', 'library': 'show', 'media_type': 'episode', 'title': 'TV Shows'} metadata = {'media_type': 'library', 'library': 'show', 'media_type_filter': 'episode', 'title': 'TV Shows'}
elif item_id == 'artist': elif item_id == 'artist':
metadata = {'type': 'library', 'library': 'artist', 'media_type': 'track', 'title': 'Music'} metadata = {'media_type': 'library', 'library': 'artist', 'media_type_filter': 'track', 'title': 'Music'}
else: else:
pms_connect = pmsconnect.PmsConnect() pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_metadata_details(rating_key=item_id) result = pms_connect.get_metadata_details(rating_key=item_id)
@ -1117,10 +1149,24 @@ class WebInterface(object):
logger.warn('Unable to retrieve data.') logger.warn('Unable to retrieve data.')
@cherrypy.expose @cherrypy.expose
def get_server_prefs(self, **kwargs): def get_server_friendly_name(self, **kwargs):
pms_connect = pmsconnect.PmsConnect() result = pmsconnect.get_server_friendly_name()
result = pms_connect.get_server_prefs(output_format='json')
if result:
cherrypy.response.headers['Content-type'] = 'application/json'
return result
else:
logger.warn('Unable to retrieve data.')
@cherrypy.expose
def get_server_prefs(self, pref=None, **kwargs):
if pref:
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_server_pref(pref=pref)
else:
result = None
if result: if result:
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'
@ -1352,8 +1398,7 @@ class WebInterface(object):
return json.dumps({'message': 'no data received'}) return json.dumps({'message': 'no data received'})
@cherrypy.expose @cherrypy.expose
def search(self, search_query=''): def search(self, query=''):
query = search_query.replace('"', '')
return serve_template(templatename="search.html", title="Search", query=query) return serve_template(templatename="search.html", title="Search", query=query)