Compare commits

...

23 Commits

Author SHA1 Message Date
JonnyWong16
331be52327 v2.1.12 2018-06-08 22:12:51 -07:00
JonnyWong16
6ece690e23 Fix grouping plays on graphs with new grouping logic 2018-06-08 18:20:51 -07:00
JonnyWong16
6b63a1399e Add collection tag to raw newsletter data 2018-06-08 17:42:20 -07:00
JonnyWong16
5c77cf652b Fix typo in Join notifier help text 2018-06-08 17:18:05 -07:00
JonnyWong16
d6f9a82edb Fix mutable default arguments 2018-06-07 18:51:52 -07:00
JonnyWong16
a887489666 Change notifier link sources to blank for disabled 2018-06-07 18:51:20 -07:00
JonnyWong16
8c0bcd0059 Ability to stop a stream using session key 2018-06-05 20:30:07 -07:00
JonnyWong16
c18ee81130 Fix typo in http handler 2018-06-03 12:25:15 -07:00
JonnyWong16
44428cc6e5 Update logger blacklist for newsleter/notifier configs 2018-06-03 11:01:58 -07:00
JonnyWong16
c19cc858bd v2.1.11-beta 2018-06-02 10:39:32 -07:00
JonnyWong16
668913fd60 Allow manual PMS URL override in config file for XML shortcuts 2018-06-02 09:55:39 -07:00
JonnyWong16
50c5407a46 Fix Monitor Remote Access checkbox (Plex server update chaged bool to int) 2018-06-02 09:32:05 -07:00
JonnyWong16
939755d3b7 Add Plex XML shortcuts to libraries, users, and sync headers 2018-06-02 08:58:55 -07:00
JonnyWong16
54f4696713 Refactor open Plex XML script 2018-06-02 08:53:53 -07:00
JonnyWong16
c85af521fe Add Plex XML shortcuts for metadata and server resources 2018-06-02 08:26:30 -07:00
JonnyWong16
917d19db85 Fix grouping or new plays not previously in history 2018-06-01 18:45:58 -07:00
JonnyWong16
7292f25eb9 Reword group play history setting 2018-05-31 09:10:44 -07:00
JonnyWong16
22a2ad4bc7 Fix progress percent in grouping logic 2018-05-31 08:34:58 -07:00
JonnyWong16
95e56f5ea5 Fix activity progress bar not updating in some cases 2018-05-31 08:33:50 -07:00
JonnyWong16
ed24232a0a Improve logic for grouping history items 2018-05-30 22:18:41 -07:00
JonnyWong16
15225faee7 Add filename to notification parameters 2018-05-30 21:52:18 -07:00
JonnyWong16
041a35a35a Fallback to title if missing index for update metadata 2018-05-30 21:46:31 -07:00
JonnyWong16
6d365c174a Update X-Plex headers 2018-05-30 21:39:45 -07:00
25 changed files with 261 additions and 115 deletions

8
API.md
View File

@@ -2624,15 +2624,15 @@ Returns:
### terminate_session
Add a new notification agent.
Stop a streaming session.
```
Required parameters:
session_id (str): The id of the session to terminate
message (str): A custom message to send to the client
session_key (int): The session key of the session to terminate, OR
session_id (str): The session id of the session to terminate
Optional parameters:
None
message (str): A custom message to send to the client
Returns:
None

View File

@@ -1,5 +1,27 @@
# Changelog
## v2.1.12 (2018-06-08)
* Notifications:
* Change: Blank notification link source means disabled instead of default.
* Newsletters:
* New: Make collection tags available in the raw newsletter data for custom templates.
* API:
* New: Ability to terminate a stream using the session key.
## v2.1.11-beta (2018-06-02)
* Monitoring:
* Fix: Activity progress bar not updating in some cases.
* Fix: Monitory Remote Access setting disabled due to Plex Media Server API changes.
* Change: Improved logic for grouping history items without being successive plays.
* Notifications:
* New: Added filename to notification parameters.
* Other:
* Fix: Update metadata failing for tracks without track numbers.
## v2.1.10-beta (2018-05-28)
* Monitoring:

View File

@@ -292,6 +292,7 @@ ${next.modalIncludes()}
<script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script>
% endif

View File

@@ -12,7 +12,7 @@
<div class="row">
<div class="col-md-12">
<div class="padded-header" id="current-activity-header">
<h3><span id="sessions-shortcut">Activity</span> &nbsp;&nbsp;
<h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp;
<small>
<span id="currentActivityHeader" style="display: none;">
Streams: <span id="currentActivityHeader-streams"></span> |
@@ -236,7 +236,6 @@
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
<script>
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
@@ -546,7 +545,7 @@
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
var progress_bar = $('#progress-bar-' + key);
progress_bar.data('state', s.state);
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
if (progress_bar.data('last_view_offset') !== s.view_offset) {
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
}
@@ -676,6 +675,7 @@
$.ajax({
url: 'terminate_session',
data: {
session_key: key,
session_id: session_id,
message: message
},
@@ -694,10 +694,8 @@
});
});
$('#sessions-shortcut').on('tripleclick', function () {
$.getJSON('return_sessions_url', function(sessions_url) {
window.open(sessions_url, '_blank');
});
$('#sessions-xml').on('tripleclick', function () {
openPlexXML('/status/sessions');
});
% endif
</script>

View File

@@ -83,31 +83,31 @@ DOCUMENTATION :: END
<ul class="list-unstyled breadcrumb">
% if data['media_type'] in ('movie', 'collection'):
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'show':
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Season ${data['media_index']}</li>
<li class="active metadata-xml">Season ${data['media_index']}</li>
% elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li>
<li class="active">Episode ${data['media_index']} - ${data['title']}</li>
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
% elif data['media_type'] == 'artist':
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">${data['title']}</li>
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Track ${data['media_index']} - ${data['title']}</li>
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
% endif
</ul>
</div>
@@ -703,6 +703,10 @@ DOCUMENTATION :: END
</script>
% endif
<script>
$('.metadata-xml').on('tripleclick', function () {
openPlexXML("/library/metadata/${data['rating_key']}");
});
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });

View File

@@ -457,4 +457,11 @@ function capitalizeFirstLetter(string) {
$.fn.slideToggleBool = function(bool, options) {
return bool ? $(this).slideDown(options) : $(this).slideUp(options);
};
};
function openPlexXML(endpoint, plextv, params) {
var data = $.extend({endpoint: endpoint, plextv: plextv}, params);
$.getJSON('return_plex_xml_url', data, function(xml_url) {
window.open(xml_url, '_blank');
});
}

View File

@@ -10,7 +10,7 @@
<div class='container-fluid'>
<div class='table-card-header'>
<div class="header-bar">
<span><i class="fa fa-book"></i> All Libraries</span>
<span id="libraries-xml"><i class="fa fa-book"></i> All Libraries</span>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
@@ -198,5 +198,9 @@
});
});
% endif
$('#libraries-xml').on('tripleclick', function () {
openPlexXML('/library/sections/all');
});
</script>
</%def>

View File

@@ -115,9 +115,9 @@
</div>
<div class="checkbox advanced-setting">
<label>
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Successive Play History
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Play History
</label>
<p class="help-block">Group successive play history by the same user as a single entry in the watch statistics, tables, and graphs.</p>
<p class="help-block">Group play history for the same item and user as a single entry when progress is less than the watched percent.</p>
</div>
<div class="checkbox advanced-setting">
<label>
@@ -646,7 +646,7 @@
<div role="tabpanel" class="tab-pane" id="tabs-plex_media_server">
<div class="padded-header">
<h3>Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
<h3 id="resources-xml">Plex Media Server <small style="color: #fff;">Version <span id="pms_version">${config['pms_version']}</span></small></h3>
</div>
<div class="form-group has-feedback" id="pms_ip_group">
@@ -2358,7 +2358,7 @@ $(document).ready(function() {
data: { pref: 'PublishServerOnPlexOnlineKey' },
async: true,
success: function(data) {
if (data !== 'true') {
if (data === 'false' || data === '0') {
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
}
@@ -2753,6 +2753,10 @@ $(document).ready(function() {
body_container.animate({scrollTop: scroll_pos});
}
});
$('#resources-xml').on('tripleclick', function () {
openPlexXML('/api/resources', true, {includeHttps: 1});
});
});
</script>
</%def>

View File

@@ -16,7 +16,7 @@
<div class='container-fluid'>
<div class='table-card-header'>
<div class="header-bar">
<span><i class="fa fa-cloud-download"></i> Synced Items</span>
<span id="sync-xml"><i class="fa fa-cloud-download"></i> Synced Items</span>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
@@ -185,5 +185,9 @@
$("#refresh-syncs-list").click(function() {
sync_table.ajax.reload();
});
$('#sync-xml').on('tripleclick', function () {
openPlexXML('/servers/{machine_id}/sync_lists', true);
});
</script>
</%def>

View File

@@ -10,7 +10,7 @@
<div class='container-fluid'>
<div class='table-card-header'>
<div class="header-bar">
<span><i class="fa fa-group"></i> All Users</span>
<span id="users-xml"><i class="fa fa-group"></i> All Users</span>
</div>
<div class="button-bar">
% if _session['user_group'] == 'admin':
@@ -202,5 +202,9 @@
});
});
% endif
$('#users-xml').on('tripleclick', function () {
openPlexXML('/api/users', true);
});
</script>
</%def>

View File

@@ -42,6 +42,7 @@ import datafactory
import libraries
import logger
import mobile_app
import newsletters
import newsletter_handler
import notification_handler
import notifiers
@@ -202,6 +203,7 @@ def initialize(config_file):
logger.error(u"Could not perform upgrades: %s" % e)
# Add notifier configs to logger blacklist
newsletters.blacklist_logger()
notifiers.blacklist_logger()
mobile_app.blacklist_logger()
@@ -516,7 +518,7 @@ def dbcheck():
# sessions table :: This is a temp table that logs currently active sessions
c_db.execute(
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, '
'CREATE TABLE IF NOT EXISTS sessions (id INTEGER PRIMARY KEY AUTOINCREMENT, session_key INTEGER, session_id TEXT, '
'transcode_key TEXT, rating_key INTEGER, section_id INTEGER, media_type TEXT, started INTEGER, stopped INTEGER, '
'paused_counter INTEGER DEFAULT 0, state TEXT, user_id INTEGER, user TEXT, friendly_name TEXT, '
'ip_address TEXT, machine_id TEXT, player TEXT, product TEXT, platform TEXT, title TEXT, parent_title TEXT, '
@@ -1079,6 +1081,15 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN live_uuid TEXT'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT session_id FROM sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN session_id TEXT'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT original_title FROM sessions')

View File

@@ -15,18 +15,13 @@
from collections import defaultdict
import json
import threading
import time
import re
import plexpy
import database
import datafactory
import helpers
import libraries
import log_reader
import logger
import notification_handler
import notifiers
import pmsconnect
import users
@@ -39,6 +34,7 @@ class ActivityProcessor(object):
def write_session(self, session=None, notify=True):
if session:
values = {'session_key': session.get('session_key', ''),
'session_id': session.get('session_id', ''),
'transcode_key': session.get('transcode_key', ''),
'section_id': session.get('section_id', ''),
'rating_key': session.get('rating_key', ''),
@@ -272,14 +268,15 @@ class ActivityProcessor(object):
self.db.upsert(table_name='session_history', key_dict=keys, value_dict=values)
# Check if we should group the session, select the last two rows from the user
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history \
WHERE user_id = ? ORDER BY id DESC LIMIT 2 '
query = 'SELECT id, rating_key, view_offset, user_id, reference_id FROM session_history ' \
'WHERE user_id = ? AND rating_key = ? ORDER BY id DESC LIMIT 2 '
args = [session['user_id']]
args = [session['user_id'], session['rating_key']]
result = self.db.select(query=query, args=args)
new_session = prev_session = None
prev_progress_percent = media_watched_percent = 0
# Get the last insert row id
last_id = self.db.last_insert_id()
@@ -296,11 +293,23 @@ class ActivityProcessor(object):
'user_id': result[1]['user_id'],
'reference_id': result[1]['reference_id']}
watched_percent = {'movie': plexpy.CONFIG.MOVIE_WATCHED_PERCENT,
'episode': plexpy.CONFIG.TV_WATCHED_PERCENT,
'track': plexpy.CONFIG.MUSIC_WATCHED_PERCENT
}
prev_progress_percent = helpers.get_percent(prev_session['view_offset'], session['duration'])
media_watched_percent = watched_percent.get(session['media_type'], 0)
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
# If previous session view offset less than watched percent,
# and new session view offset is greater,
# then set the reference_id to the previous row,
# else set the reference_id to the new id
if prev_session is None and new_session is None:
args = [last_id, last_id]
elif prev_session['rating_key'] == new_session['rating_key'] and prev_session['view_offset'] <= new_session['view_offset']:
elif prev_progress_percent < media_watched_percent and \
prev_session['view_offset'] <= new_session['view_offset']:
args = [prev_session['reference_id'], new_session['id']]
else:
args = [new_session['id'], new_session['id']]
@@ -458,6 +467,16 @@ class ActivityProcessor(object):
return None
def get_session_by_id(self, session_id=None):
if session_id:
session = self.db.select_single('SELECT * FROM sessions '
'WHERE session_id = ? ',
args=[session_id])
if session:
return session
return None
def set_session_state(self, session_key=None, state=None, **kwargs):
if str(session_key).isdigit():
values = {}

View File

@@ -23,6 +23,7 @@ PLATFORM = platform.system()
PLATFORM_RELEASE = platform.release()
PLATFORM_VERSION = platform.version()
PLATFORM_LINUX_DISTRO = ' '.join(x for x in platform.linux_distribution() if x)
PLATFORM_DEVICE_NAME = platform.node()
BRANCH = version.PLEXPY_BRANCH
RELEASE = version.PLEXPY_RELEASE_VERSION
@@ -475,6 +476,7 @@ NOTIFICATION_PARAMETERS = [
{'name': 'Subtitle Language', 'type': 'str', 'value': 'subtitle_language', 'description': 'The subtitle language of the original media.'},
{'name': 'Subtitle Language Code', 'type': 'str', 'value': 'subtitle_language_code', 'description': 'The subtitle language code of the original media.'},
{'name': 'File', 'type': 'str', 'value': 'file', 'description': 'The file path to the item.'},
{'name': 'Filename', 'type': 'str', 'value': 'filename', 'description': 'The file name of the item.'},
{'name': 'File Size', 'type': 'int', 'value': 'file_size', 'description': 'The file size of the item.'},
{'name': 'Section ID', 'type': 'int', 'value': 'section_id', 'description': 'The unique identifier for the library.'},
{'name': 'Rating Key', 'type': 'int', 'value': 'rating_key', 'description': 'The unique identifier for the movie, episode, or track.'},

View File

@@ -54,6 +54,7 @@ _CONFIG_DEFINITIONS = {
'PMS_TOKEN': (str, 'PMS', ''),
'PMS_SSL': (int, 'PMS', 0),
'PMS_URL': (str, 'PMS', ''),
'PMS_URL_OVERRIDE': (str, 'PMS', ''),
'PMS_URL_MANUAL': (int, 'PMS', 0),
'PMS_USE_BIF': (int, 'PMS', 0),
'PMS_UUID': (str, 'PMS', ''),

View File

@@ -65,7 +65,7 @@ class DataFactory(object):
columns = [
'session_history.reference_id',
'session_history.id',
'started AS date',
'MAX(started) AS date',
'MIN(started) AS started',
'MAX(stopped) AS stopped',
'SUM(CASE WHEN stopped > 0 THEN (stopped - started) ELSE 0 END) - \
@@ -1467,7 +1467,7 @@ class DataFactory(object):
result = monitor_db.select(query=query.format('parent_rating_key', 'rating_key'),
args=[item['parent_rating_key']])
for item in result:
key = item['media_index']
key = item['media_index'] if item['media_index'] else item['title']
children.update({key: {'rating_key': item['rating_key']}})
key = item['parent_media_index'] if match_type == 'index' else item['parent_title']

View File

@@ -50,7 +50,9 @@ class Graphs(object):
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'FROM (SELECT * FROM session_history ' \
'GROUP BY date(started, "unixepoch", "localtime"), %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY date_played ' \
'ORDER BY started ASC' % (group_by, time_range, user_cond)
@@ -147,7 +149,9 @@ class Graphs(object):
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'FROM (SELECT * FROM session_history ' \
'GROUP BY strftime("%%w", datetime(started, "unixepoch", "localtime")), %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY dayofweek ' \
'ORDER BY daynumber' % (group_by, time_range, user_cond)
@@ -245,7 +249,9 @@ class Graphs(object):
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'FROM (SELECT * FROM session_history ' \
'GROUP BY strftime("%%H", datetime(started, "unixepoch", "localtime")) , %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") %s' \
'GROUP BY hourofday ' \
'ORDER BY hourofday' % (group_by, time_range, user_cond)
@@ -335,7 +341,9 @@ class Graphs(object):
'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 = "track" THEN 1 ELSE 0 END) AS music_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'FROM (SELECT * FROM session_history ' \
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")), %s) ' \
'AS session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-%s months", "localtime") %s' \
'GROUP BY strftime("%%Y-%%m", datetime(started, "unixepoch", "localtime")) ' \
'ORDER BY datestring DESC LIMIT %s' % (group_by, time_range, user_cond, time_range)
@@ -591,7 +599,9 @@ class Graphs(object):
'THEN 1 ELSE 0 END) AS ds_count, ' \
'SUM(CASE WHEN session_history_media_info.transcode_decision = "transcode" ' \
'THEN 1 ELSE 0 END) AS tc_count ' \
'FROM (SELECT * FROM session_history GROUP BY %s) AS session_history ' \
'FROM (SELECT * FROM session_history ' \
'GROUP BY date(session_history.started, "unixepoch", "localtime"), %s) ' \
'AS session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(started, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \

View File

@@ -39,12 +39,13 @@ class HTTPHandler(object):
else:
self.urls = urls
self.headers = {'X-Plex-Device-Name': 'Tautulli',
'X-Plex-Product': 'Tautulli',
self.headers = {'X-Plex-Product': 'Tautulli',
'X-Plex-Version': plexpy.common.RELEASE,
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
'X-Plex-Platform': plexpy.common.PLATFORM,
'X-Plex-Platform-Version': plexpy.common.PLATFORM_RELEASE,
'X-Plex-Client-Identifier': plexpy.CONFIG.PMS_UUID,
'X-Plex-Device': 'Web',
'X-Plex-Device-Name': plexpy.common.PLATFORM_DEVICE_NAME
}
self.token = token
@@ -178,5 +179,5 @@ class HTTPHandler(object):
return output
except Exception as e:
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.response_type, e))
logger.warn(u"Failed format response from uri %s to %s error %s" % (self.uri, self.output_format, e))
return None

View File

@@ -28,6 +28,7 @@ import traceback
import plexpy
import helpers
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
# These settings are for file logging only
FILENAME = "tautulli.log"
@@ -48,6 +49,20 @@ logger_plex_websocket = logging.getLogger("plex_websocket")
# Global queue for multiprocessing logging
queue = None
def blacklist_config(config):
blacklist = set()
blacklist_keys = ['HOOK', 'APIKEY', 'KEY', 'PASSWORD', 'TOKEN']
for key, value in config.iteritems():
if isinstance(value, basestring) and len(value.strip()) > 5 and \
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or
any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
blacklist.add(value.strip())
_BLACKLIST_WORDS.update(blacklist)
class NoThreadFilter(logging.Filter):
"""
Log filter for the current thread

View File

@@ -138,7 +138,5 @@ def set_last_seen(device_token=None):
def blacklist_logger():
devices = get_mobile_devices()
blacklist = set(d['device_token'] for d in devices)
logger._BLACKLIST_WORDS.update(blacklist)
for d in devices:
logger.blacklist_config(d)

View File

@@ -196,6 +196,7 @@ def add_newsletter_config(agent_id=None, **kwargs):
newsletter_id = db.last_insert_id()
logger.info(u"Tautulli Newsletters :: Added new newsletter agent: %s (newsletter_id %s)."
% (agent['label'], newsletter_id))
blacklist_logger()
return newsletter_id
except Exception as e:
logger.warn(u"Tautulli Newsletters :: Unable to add newsletter agent: %s." % e)
@@ -254,6 +255,7 @@ def set_newsletter_config(newsletter_id=None, agent_id=None, **kwargs):
logger.info(u"Tautulli Newsletters :: Updated newsletter agent: %s (newsletter_id %s)."
% (agent['label'], newsletter_id))
newsletter_handler.schedule_newsletters(newsletter_id=newsletter_id)
blacklist_logger()
return True
except Exception as e:
logger.warn(u"Tautulli Newsletters :: Unable to update newsletter agent: %s." % e)
@@ -274,6 +276,17 @@ def send_newsletter(newsletter_id=None, subject=None, body=None, message=None, n
logger.debug(u"Tautulli Newsletters :: Notification requested but no newsletter_id received.")
def blacklist_logger():
db = database.MonitorDatabase()
notifiers = db.select('SELECT newsletter_config, email_config FROM newsletters')
for n in notifiers:
config = json.loads(n['newsletter_config'] or '{}')
logger.blacklist_config(config)
email_config = json.loads(n['email_config'] or '{}')
logger.blacklist_config(email_config)
def serve_template(templatename, **kwargs):
if plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR:
template_dir = plexpy.CONFIG.NEWSLETTER_CUSTOM_DIR

View File

@@ -883,6 +883,7 @@ def build_media_notify_params(notify_action=None, session=None, timeline=None, m
'subtitle_language': notify_params['subtitle_language'],
'subtitle_language_code': notify_params['subtitle_language_code'],
'file': notify_params['file'],
'filename': os.path.basename(notify_params['file']),
'file_size': helpers.humanFileSize(notify_params['file_size']),
'indexes': notify_params['indexes'],
'section_id': notify_params['section_id'],

View File

@@ -62,7 +62,6 @@ import mobile_app
import pmsconnect
import request
import users
from plexpy.config import _BLACKLIST_KEYS, _WHITELIST_KEYS
BROWSER_NOTIFIERS = {}
@@ -612,17 +611,9 @@ def blacklist_logger():
db = database.MonitorDatabase()
notifiers = db.select('SELECT notifier_config FROM notifiers')
blacklist = set()
blacklist_keys = ['hook', 'key', 'password', 'token']
for n in notifiers:
config = json.loads(n['notifier_config'] or '{}')
for key, value in config.iteritems():
if isinstance(value, basestring) and len(value.strip()) > 5 and \
key.upper() not in _WHITELIST_KEYS and (key.upper() in blacklist_keys or any(bk in key.upper() for bk in _BLACKLIST_KEYS)):
blacklist.add(value.strip())
logger._BLACKLIST_WORDS.update(blacklist)
logger.blacklist_config(config)
class PrettyMetadata(object):
@@ -682,13 +673,13 @@ class PrettyMetadata(object):
provider_name = 'Trakt.tv'
elif provider == 'lastfm':
provider_name = 'Last.fm'
else:
if self.media_type == 'movie':
provider_name = 'IMDb'
elif self.media_type in ('show', 'season', 'episode'):
provider_name = 'TheTVDB'
elif self.media_type in ('artist', 'album', 'track'):
provider_name = 'Last.fm'
# else:
# if self.media_type == 'movie':
# provider_name = 'IMDb'
# elif self.media_type in ('show', 'season', 'episode'):
# provider_name = 'TheTVDB'
# elif self.media_type in ('artist', 'album', 'track'):
# provider_name = 'Last.fm'
return provider_name
def get_provider_link(self, provider=None):
@@ -697,13 +688,13 @@ class PrettyMetadata(object):
provider_link = self.get_plex_url()
elif provider:
provider_link = self.parameters.get(provider + '_url', '')
else:
if self.media_type == 'movie':
provider_link = self.parameters.get('imdb_url', '')
elif self.media_type in ('show', 'season', 'episode'):
provider_link = self.parameters.get('thetvdb_url', '')
elif self.media_type in ('artist', 'album', 'track'):
provider_link = self.parameters.get('lastfm_url', '')
# else:
# if self.media_type == 'movie':
# provider_link = self.parameters.get('imdb_url', '')
# elif self.media_type in ('show', 'season', 'episode'):
# provider_link = self.parameters.get('thetvdb_url', '')
# elif self.media_type in ('artist', 'album', 'track'):
# provider_link = self.parameters.get('lastfm_url', '')
return provider_link
def get_caption(self, provider):
@@ -711,6 +702,7 @@ class PrettyMetadata(object):
return 'View on ' + provider_name
def get_title(self, divider='-'):
title = ''
if self.media_type == 'movie':
title = '%s (%s)' % (self.parameters['title'], self.parameters['year'])
elif self.media_type == 'show':
@@ -1251,7 +1243,7 @@ class DISCORD(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'discord_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -1259,7 +1251,7 @@ class DISCORD(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'discord_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -1267,7 +1259,7 @@ class DISCORD(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'discord_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.',
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -1600,7 +1592,7 @@ class FACEBOOK(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'facebook_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -1608,7 +1600,7 @@ class FACEBOOK(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'facebook_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -1616,7 +1608,7 @@ class FACEBOOK(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'facebook_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.',
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -1936,7 +1928,7 @@ class HIPCHAT(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'hipchat_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -1944,7 +1936,7 @@ class HIPCHAT(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'hipchat_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -1952,7 +1944,7 @@ class HIPCHAT(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'hipchat_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.',
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -2144,7 +2136,7 @@ class JOIN(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'join_movie_provider',
'description': 'Select the source for movie links in the notificaation. Leave blank for default.<br>'
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -2152,7 +2144,7 @@ class JOIN(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'join_tv_provider',
'description': 'Select the source for tv show links in the notificaation. Leave blank for default.<br>'
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -2160,7 +2152,7 @@ class JOIN(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'join_music_provider',
'description': 'Select the source for music links in the notificaation. Leave blank for default.',
'description': 'Select the source for music links in the notification. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -2450,7 +2442,8 @@ class PLEX(Notifier):
else:
return request.request_content(url)
def _sendjson(self, host, method, params={}):
def _sendjson(self, host, method, params=None):
params = params or {}
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc'
@@ -2928,7 +2921,7 @@ class PUSHOVER(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'pushover_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -2936,7 +2929,7 @@ class PUSHOVER(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'pushover_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -2944,7 +2937,7 @@ class PUSHOVER(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'pushover_music_provider',
'description': 'Select the source for music links in the notification. Leave blank for default.',
'description': 'Select the source for music links in the notification. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -3303,7 +3296,7 @@ class SLACK(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'slack_movie_provider',
'description': 'Select the source for movie links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for movie links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -3311,7 +3304,7 @@ class SLACK(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'slack_tv_provider',
'description': 'Select the source for tv show links on the info cards. Leave blank for default.<br>'
'description': 'Select the source for tv show links on the info cards. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -3319,7 +3312,7 @@ class SLACK(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'slack_music_provider',
'description': 'Select the source for music links on the info cards. Leave blank for default.',
'description': 'Select the source for music links on the info cards. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}
@@ -3553,7 +3546,8 @@ class XBMC(Notifier):
else:
return request.request_content(url)
def _sendjson(self, host, method, params={}):
def _sendjson(self, host, method, params=None):
params = params or {}
data = [{'id': 0, 'jsonrpc': '2.0', 'method': method, 'params': params}]
headers = {'Content-Type': 'application/json'}
url = host + '/jsonrpc'
@@ -3714,7 +3708,7 @@ class ZAPIER(Notifier):
{'label': 'Movie Link Source',
'value': self.config['movie_provider'],
'name': 'zapier_movie_provider',
'description': 'Select the source for movie links in the notification. Leave blank for default.<br>'
'description': 'Select the source for movie links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_movie_providers()
@@ -3722,7 +3716,7 @@ class ZAPIER(Notifier):
{'label': 'TV Show Link Source',
'value': self.config['tv_provider'],
'name': 'zapier_tv_provider',
'description': 'Select the source for tv show links in the notification. Leave blank for default.<br>'
'description': 'Select the source for tv show links in the notification. Leave blank to disable.<br>'
'Note: 3rd party API lookup may need to be enabled under the notifications settings tab.',
'input_type': 'select',
'select_options': PrettyMetadata().get_tv_providers()
@@ -3730,7 +3724,7 @@ class ZAPIER(Notifier):
{'label': 'Music Link Source',
'value': self.config['music_provider'],
'name': 'zapier_music_provider',
'description': 'Select the source for music links in the notification. Leave blank for default.',
'description': 'Select the source for music links in the notification. Leave blank to disable.',
'input_type': 'select',
'select_options': PrettyMetadata().get_music_providers()
}

View File

@@ -19,6 +19,7 @@ import time
import urllib
import plexpy
import activity_processor
import common
import helpers
import http_handler
@@ -482,6 +483,7 @@ class PmsConnect(object):
actors = []
genres = []
labels = []
collections = []
if m.getElementsByTagName('Director'):
for director in m.getElementsByTagName('Director'):
@@ -503,6 +505,10 @@ class PmsConnect(object):
for label in m.getElementsByTagName('Label'):
labels.append(helpers.get_xml_attr(label, 'tag'))
if m.getElementsByTagName('Collection'):
for collection in m.getElementsByTagName('Collection'):
collections.append(helpers.get_xml_attr(collection, 'tag'))
recent_item = {'media_type': helpers.get_xml_attr(m, 'type'),
'section_id': helpers.get_xml_attr(m, 'librarySectionID'),
'library_name': helpers.get_xml_attr(m, 'librarySectionTitle'),
@@ -540,6 +546,7 @@ class PmsConnect(object):
'actors': actors,
'genres': genres,
'labels': labels,
'collections': collections,
'full_title': helpers.get_xml_attr(m, 'title'),
'child_count': helpers.get_xml_attr(m, 'childCount')
}
@@ -1919,7 +1926,7 @@ class PmsConnect(object):
return session_output
def terminate_session(self, session_id='', message=''):
def terminate_session(self, session_key='', session_id='', message=''):
"""
Terminates a streaming session.
@@ -1927,10 +1934,22 @@ class PmsConnect(object):
"""
message = message or 'The server owner has ended the stream.'
if session_key and not session_id:
ap = activity_processor.ActivityProcessor()
session = ap.get_session_by_key(session_key=session_key)
session_id = session['session_id']
elif session_id and not session_key:
ap = activity_processor.ActivityProcessor()
session = ap.get_session_by_id(session_id=session_id)
session_key = session['session_key']
if session_id:
logger.info(u"Tautulli Pmsconnect :: Terminating session %s (session_id %s)." % (session_key, session_id))
result = self.get_sessions_terminate(session_id=session_id, reason=urllib.quote_plus(message))
return result
else:
logger.warn(u"Tautulli Pmsconnect :: Failed to terminate session %s. Missing session_id." % session_key)
return False
def get_item_children(self, rating_key='', get_grandchildren=False):
@@ -2664,7 +2683,7 @@ class PmsConnect(object):
child_title = helpers.get_xml_attr(item, 'title')
if child_rating_key:
key = int(child_index)
key = int(child_index) if child_index else child_title
children.update({key: {'rating_key': int(child_rating_key)}})
key = int(parent_index) if match_type == 'index' else parent_title
@@ -2676,9 +2695,9 @@ class PmsConnect(object):
key = 0 if match_type == 'index' else title
key_list = {key: {'rating_key': int(rating_key),
'children': parents},
'section_id': section_id,
'library_name': library_name
}
'section_id': section_id,
'library_name': library_name
}
return key_list

View File

@@ -1,2 +1,2 @@
PLEXPY_BRANCH = "beta"
PLEXPY_RELEASE_VERSION = "v2.1.10-beta"
PLEXPY_BRANCH = "master"
PLEXPY_RELEASE_VERSION = "v2.1.12"

View File

@@ -18,6 +18,7 @@ import json
import os
import shutil
import threading
import urllib
import cherrypy
from cherrypy.lib.static import serve_file, serve_download
@@ -247,23 +248,23 @@ class WebInterface(object):
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
@addtoapi()
def terminate_session(self, session_id=None, message=None, **kwargs):
""" Add a new notification agent.
def terminate_session(self, session_key=None, session_id=None, message=None, **kwargs):
""" Stop a streaming session.
```
Required parameters:
session_id (str): The id of the session to terminate
message (str): A custom message to send to the client
session_key (int): The session key of the session to terminate, OR
session_id (str): The session id of the session to terminate
Optional parameters:
None
message (str): A custom message to send to the client
Returns:
None
```
"""
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.terminate_session(session_id=session_id, message=message)
result = pms_connect.terminate_session(session_key=session_key, session_id=session_id, message=message)
if result:
return {'result': 'success', 'message': 'Session terminated.'}
@@ -273,8 +274,21 @@ class WebInterface(object):
@cherrypy.expose
@cherrypy.tools.json_out()
@requireAuth(member_of("admin"))
def return_sessions_url(self, **kwargs):
return plexpy.CONFIG.PMS_URL + '/status/sessions?X-Plex-Token=' + plexpy.CONFIG.PMS_TOKEN
def return_plex_xml_url(self, endpoint='', plextv=False, **kwargs):
kwargs['X-Plex-Token'] = plexpy.CONFIG.PMS_TOKEN
if plextv:
base_url = 'https://plex.tv'
else:
if plexpy.CONFIG.PMS_URL_OVERRIDE:
base_url = plexpy.CONFIG.PMS_URL_OVERRIDE
else:
base_url = plexpy.CONFIG.PMS_URL
if '{machine_id}' in endpoint:
endpoint = endpoint.format(machine_id=plexpy.CONFIG.PMS_IDENTIFIER)
return base_url + endpoint + '?' + urllib.urlencode(kwargs)
@cherrypy.expose
@requireAuth()