mirror of
https://github.com/Tautulli/Tautulli.git
synced 2025-07-06 05:01:14 -07:00
Implement new home page stats (still needs styling)
This commit is contained in:
parent
9364b06c99
commit
112d5f0efa
4 changed files with 267 additions and 1 deletions
67
data/interfaces/default/home_stats.html
Normal file
67
data/interfaces/default/home_stats.html
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
% if stats[0]['rows']:
|
||||||
|
<div class="dashboard-recent-media-row">
|
||||||
|
<ul class="dashboard-recent-media">
|
||||||
|
% for a in stats:
|
||||||
|
<div class="dashboard-recent-media-instance">
|
||||||
|
% if a['stat_id'] == 'top_tv':
|
||||||
|
<li>
|
||||||
|
<div class="poster">
|
||||||
|
<h5>Most watched TV</h5><br/>
|
||||||
|
<div class="poster-face">
|
||||||
|
<a href="info?rating_key=${a['rows'][0]['rating_key']}">
|
||||||
|
% if a['rows'][0]['grandparent_thumb'] != '':
|
||||||
|
<img src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=153&height=225" class="poster-face">
|
||||||
|
% else:
|
||||||
|
<img src="interfaces/default/images/poster.png" class="poster-face">
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
|
<h3>${a['rows'][0]['orig_title']}</h3>
|
||||||
|
<div class="muted">Plays: ${a['rows'][0]['total_plays']}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% elif a['stat_id'] == 'top_users':
|
||||||
|
<li>
|
||||||
|
<div class="poster">
|
||||||
|
<h5>Most active user</h5><br/>
|
||||||
|
<div class="poster-face">
|
||||||
|
<a href="user?user=${a['rows'][0]['user']}">
|
||||||
|
% if a['rows'][0]['thumb'] != '':
|
||||||
|
<img src="${a['rows'][0]['thumb']}" class="poster-face">
|
||||||
|
% else:
|
||||||
|
<img src="interfaces/default/images/gravatar-default.png" class="poster-face">
|
||||||
|
% endif
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
|
<h3>${a['rows'][0]['user']}</h3>
|
||||||
|
<div class="muted">Plays: ${a['rows'][0]['total_plays']}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
% elif a['stat_id'] == 'top_platforms':
|
||||||
|
<li>
|
||||||
|
<div class="poster">
|
||||||
|
<h5>Most used platform</h5><br/>
|
||||||
|
<div class="poster-face" id="platform-face">
|
||||||
|
<img src="interfaces/default/images/platforms/default.png" class="poster-face">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="dashboard-recent-media-metacontainer">
|
||||||
|
<h3>${a['rows'][0]['platform_type']}</h3>
|
||||||
|
<div class="muted">Plays: ${a['rows'][0]['total_plays']}</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<script>
|
||||||
|
$("#platform-face").html("<img class='user-platforms-instance-poster' src='" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + "'>");
|
||||||
|
</script>
|
||||||
|
% endif
|
||||||
|
</div>
|
||||||
|
% endfor
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
% else:
|
||||||
|
<div class="muted">No stats for selected period.</div><br>
|
||||||
|
% endif
|
|
@ -26,6 +26,24 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12">
|
||||||
|
<div class="wellbg">
|
||||||
|
<div class="wellheader">
|
||||||
|
<div class="dashboard-wellheader">
|
||||||
|
<h3>Statistics
|
||||||
|
<span class="muted"> | <a href="javascript:void(0)" id="stats-7">7 days</a></span>
|
||||||
|
<span class="muted"> | <a href="javascript:void(0)" id="stats-30">30 days</a></span>
|
||||||
|
<span class="muted"> | <a href="javascript:void(0)" id="stats-90">90 days</a></span>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="home-stats">
|
||||||
|
<div class='muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div><br>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class='row-fluid'>
|
<div class='row-fluid'>
|
||||||
<div class='wellbg'>
|
<div class='wellbg'>
|
||||||
<div class='wellheader'>
|
<div class='wellheader'>
|
||||||
|
@ -45,6 +63,19 @@
|
||||||
<%def name="javascriptIncludes()">
|
<%def name="javascriptIncludes()">
|
||||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
function getHomeStats(days) {
|
||||||
|
$.ajax({
|
||||||
|
url: 'home_stats',
|
||||||
|
cache: false,
|
||||||
|
async: true,
|
||||||
|
data: {time_range: days},
|
||||||
|
complete: function(xhr, status) {
|
||||||
|
$("#home-stats").html(xhr.responseText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function currentActivity() {
|
function currentActivity() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: 'get_current_activity',
|
url: 'get_current_activity',
|
||||||
|
@ -97,6 +128,24 @@
|
||||||
recentlyAdded();
|
recentlyAdded();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getHomeStats(7);
|
||||||
|
|
||||||
|
$('#stats-7').click(function() {
|
||||||
|
$('#home-stats').html('<div class="muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div><br>');
|
||||||
|
getHomeStats(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stats-30').click(function() {
|
||||||
|
$('#home-stats').html('<div class="muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div><br>');
|
||||||
|
getHomeStats(30);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#stats-90').click(function() {
|
||||||
|
$('#home-stats').html('<div class="muted"><i class="fa fa-refresh fa-spin"></i> Loading stats...</div><br>');
|
||||||
|
getHomeStats(90);
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
</%def>
|
</%def>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
from plexpy import logger, helpers, request, datatables, config, db
|
from plexpy import logger, helpers, request, datatables, config, db
|
||||||
from xml.dom import minidom
|
from xml.dom import minidom
|
||||||
|
from collections import defaultdict, Counter
|
||||||
|
|
||||||
import plexpy
|
import plexpy
|
||||||
import json
|
import json
|
||||||
|
@ -562,4 +563,134 @@ class PlexWatch(object):
|
||||||
user_info = {'user_id': user_id,
|
user_info = {'user_id': user_id,
|
||||||
'user_thumb': user_thumb}
|
'user_thumb': user_thumb}
|
||||||
|
|
||||||
return user_info
|
return user_info
|
||||||
|
|
||||||
|
def get_home_stats(self, time_range='30'):
|
||||||
|
myDB = db.DBConnection()
|
||||||
|
|
||||||
|
if not time_range.isdigit():
|
||||||
|
time_range = '30'
|
||||||
|
|
||||||
|
stats_queries = ["top_tv", "top_users", "top_platforms"]
|
||||||
|
home_stats = []
|
||||||
|
|
||||||
|
for stat in stats_queries:
|
||||||
|
if 'top_tv' in stat:
|
||||||
|
top_tv = []
|
||||||
|
query = 'SELECT orig_title, COUNT(orig_title) as total_plays, grandparentRatingKey, MAX(time) as last_watch, xml ' \
|
||||||
|
'FROM %s ' \
|
||||||
|
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||||
|
'AND episode != "" ' \
|
||||||
|
'GROUP BY orig_title ' \
|
||||||
|
'ORDER BY total_plays DESC LIMIT 10' % (self.get_user_table_name(), time_range)
|
||||||
|
result = myDB.select(query)
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
xml_data = helpers.latinToAscii(item[4])
|
||||||
|
|
||||||
|
try:
|
||||||
|
xml_parse = minidom.parseString(xml_data)
|
||||||
|
except:
|
||||||
|
logger.warn("Error parsing XML for Plexwatch database.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
xml_head = xml_parse.getElementsByTagName('opt')
|
||||||
|
if not xml_head:
|
||||||
|
logger.warn("Error parsing XML for Plexwatch database.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for a in xml_head:
|
||||||
|
grandparent_thumb = self.get_xml_attr(a, 'grandparentThumb')
|
||||||
|
|
||||||
|
row = {'orig_title': item[0],
|
||||||
|
'total_plays': item[1],
|
||||||
|
'rating_key': item[2],
|
||||||
|
'last_play': item[3],
|
||||||
|
'grandparent_thumb': grandparent_thumb
|
||||||
|
}
|
||||||
|
top_tv.append(row)
|
||||||
|
|
||||||
|
home_stats.append({'stat_id': stat,
|
||||||
|
'rows': top_tv})
|
||||||
|
|
||||||
|
elif 'top_users' in stat:
|
||||||
|
top_users = []
|
||||||
|
query = 'SELECT user, COUNT(id) as total_plays, MAX(time) as last_watch ' \
|
||||||
|
'FROM %s ' \
|
||||||
|
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||||
|
'GROUP BY user ' \
|
||||||
|
'ORDER BY total_plays DESC LIMIT 10' % (self.get_user_table_name(), time_range)
|
||||||
|
result = myDB.select(query)
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
thumb = self.get_user_gravatar_image(item[0])
|
||||||
|
row = {'user': item[0],
|
||||||
|
'total_plays': item[1],
|
||||||
|
'last_play': item[2],
|
||||||
|
'thumb': thumb['user_thumb']
|
||||||
|
}
|
||||||
|
top_users.append(row)
|
||||||
|
|
||||||
|
home_stats.append({'stat_id': stat,
|
||||||
|
'rows': top_users})
|
||||||
|
|
||||||
|
elif 'top_platforms' in stat:
|
||||||
|
top_platform = []
|
||||||
|
query = 'SELECT platform, COUNT(id) as total_plays, MAX(time) as last_watch, xml ' \
|
||||||
|
'FROM %s ' \
|
||||||
|
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
|
||||||
|
'GROUP BY platform ' \
|
||||||
|
'ORDER BY total_plays DESC' % (self.get_user_table_name(), time_range)
|
||||||
|
result = myDB.select(query)
|
||||||
|
|
||||||
|
for item in result:
|
||||||
|
xml_data = helpers.latinToAscii(item[3])
|
||||||
|
|
||||||
|
try:
|
||||||
|
xml_parse = minidom.parseString(xml_data)
|
||||||
|
except:
|
||||||
|
logger.warn("Error parsing XML for Plexwatch database.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
xml_head = xml_parse.getElementsByTagName('Player')
|
||||||
|
if not xml_head:
|
||||||
|
logger.warn("Error parsing XML for Plexwatch database.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
for a in xml_head:
|
||||||
|
platform_type = self.get_xml_attr(a, 'platform')
|
||||||
|
|
||||||
|
row = {'platform': item[0],
|
||||||
|
'total_plays': item[1],
|
||||||
|
'last_play': item[2],
|
||||||
|
'platform_type': platform_type
|
||||||
|
}
|
||||||
|
top_platform.append(row)
|
||||||
|
|
||||||
|
top_platform_aggr = self.group_and_sum_dataset(
|
||||||
|
top_platform, 'platform_type', ['total_plays'], 'total_plays')
|
||||||
|
|
||||||
|
home_stats.append({'stat_id': stat,
|
||||||
|
'rows': top_platform_aggr})
|
||||||
|
|
||||||
|
return home_stats
|
||||||
|
|
||||||
|
# Taken from:
|
||||||
|
# https://stackoverflow.com/questions/18066269/group-by-and-aggregate-the-values-of-a-list-of-dictionaries-in-python
|
||||||
|
@staticmethod
|
||||||
|
def group_and_sum_dataset(dataset, group_by_key, sum_value_keys, sort_by_key):
|
||||||
|
|
||||||
|
container = defaultdict(Counter)
|
||||||
|
|
||||||
|
for item in dataset:
|
||||||
|
key = item[group_by_key]
|
||||||
|
values = {k: item[k] for k in sum_value_keys}
|
||||||
|
container[key].update(values)
|
||||||
|
|
||||||
|
new_dataset = [
|
||||||
|
dict([(group_by_key, item[0])] + item[1].items())
|
||||||
|
for item in container.items()
|
||||||
|
]
|
||||||
|
new_dataset.sort(key=lambda item: item[sort_by_key], reverse=True)
|
||||||
|
|
||||||
|
return new_dataset
|
|
@ -83,6 +83,13 @@ class WebInterface(object):
|
||||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||||
return json.dumps(formats)
|
return json.dumps(formats)
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def home_stats(self, time_range='30', **kwargs):
|
||||||
|
plex_watch = plexwatch.PlexWatch()
|
||||||
|
stats_data = plex_watch.get_home_stats(time_range)
|
||||||
|
|
||||||
|
return serve_template(templatename="home_stats.html", title="Stats", stats=stats_data)
|
||||||
|
|
||||||
@cherrypy.expose
|
@cherrypy.expose
|
||||||
def history(self):
|
def history(self):
|
||||||
return serve_template(templatename="history.html", title="History")
|
return serve_template(templatename="history.html", title="History")
|
||||||
|
@ -671,6 +678,18 @@ class WebInterface(object):
|
||||||
plex_watch = plexwatch.PlexWatch()
|
plex_watch = plexwatch.PlexWatch()
|
||||||
result = plex_watch.get_user_gravatar_image(user)
|
result = plex_watch.get_user_gravatar_image(user)
|
||||||
|
|
||||||
|
if result:
|
||||||
|
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||||
|
return json.dumps(result)
|
||||||
|
else:
|
||||||
|
logger.warn('Unable to retrieve data.')
|
||||||
|
|
||||||
|
@cherrypy.expose
|
||||||
|
def get_home_stats(self, time_range='30', **kwargs):
|
||||||
|
|
||||||
|
plex_watch = plexwatch.PlexWatch()
|
||||||
|
result = plex_watch.get_home_stats(time_range)
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||||
return json.dumps(result)
|
return json.dumps(result)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue