diff --git a/data/interfaces/default/history.html b/data/interfaces/default/history.html index 521fb8a4..17f8a76e 100644 --- a/data/interfaces/default/history.html +++ b/data/interfaces/default/history.html @@ -46,53 +46,6 @@ @@ -107,213 +60,15 @@ + + history_table = $('#history_table').DataTable(history_table_options); + }); + diff --git a/data/interfaces/default/info.html b/data/interfaces/default/info.html index 5718317f..b8eabe23 100644 --- a/data/interfaces/default/info.html +++ b/data/interfaces/default/info.html @@ -4,7 +4,7 @@ %> <%def name="headIncludes()"> - + <%def name="body()"> @@ -52,12 +52,12 @@
% if metadata['type'] == 'episode' or metadata['type'] == 'movie': % if metadata['directors']: - Directed by ${metadata['directors'][0]} + Directed by ${metadata['directors'][0]} % else: - Directed by unknown + Directed by unknown % endif % elif metadata['type'] == 'show': - Studio ${metadata['studio']} + Studio ${metadata['studio']} % endif
@@ -138,6 +138,44 @@
+
+
+
+
+ % if metadata['type'] == 'movie' or metadata['type'] == 'episode': +
+
+

Watch history for ${metadata['title']}

+
+
+ + + + + + + + + + + + + + + + + + + + +
ID Time User Platform IP Address Title Started Paused Stopped Duration Completed RatingKey
+ + % endif +
+
+
+
% else:
@@ -152,6 +190,10 @@ <%def name="javascriptIncludes()"> + + + + % if metadata: % if metadata['type'] == 'movie': % endif + % if metadata['type'] == 'movie' or metadata['type'] == 'episode': + + + %endif % endif diff --git a/data/interfaces/default/js/tables/history_table.js b/data/interfaces/default/js/tables/history_table.js new file mode 100644 index 00000000..fc02822a --- /dev/null +++ b/data/interfaces/default/js/tables/history_table.js @@ -0,0 +1,180 @@ +var date_format = 'YYYY-MM-DD'; +var time_format = 'hh:mm a'; + +$.ajax({ + url: 'get_date_formats', + type: 'GET', + success: function(data) { + date_format = data.date_format; + time_format = data.time_format; + } +}); + +history_table_options = { + "destroy": true, + "language": { + "search": "Search: ", + "lengthMenu":"Show _MENU_ entries per page", + "info":"Showing _START_ to _END_ of _TOTAL_ history items", + "infoEmpty":"Showing 0 to 0 of 0 entries", + "infoFiltered":"(filtered from _MAX_ total entries)", + "emptyTable": "No data in table", + }, + "stateSave": true, + "sPaginationType": "bootstrap", + "processing": false, + "serverSide": true, + "pageLength": 10, + "order": [ 1, 'desc'], + "columnDefs": [ + { + "targets": [0], + "data":"id", + "visible": false, + "searchable": false + }, + { + "targets": [1], + "data":"date", + "createdCell": function (td, cellData, rowData, row, col) { + if (rowData['stopped'] === null) { + $(td).addClass('currentlyWatching'); + $(td).html('Currently watching...'); + } else { + $(td).html(moment(cellData,"X").format(date_format)); + } + }, + "searchable": false + }, + { + "targets": [2], + "data":"user" + }, + { + "targets": [3], + "data":"platform", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== '') { + $(td).html(' '+cellData); + } + }, + "className": "modal-control" + }, + { + "targets": [4], + "data":"ip_address", + "createdCell": function (td, cellData, rowData, row, col) { + if ((cellData == '') || (cellData == '0')) { + $(td).html('n/a'); + } + } + }, + { + "targets": [5], + "data":"title", + "name":"title", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== '') { + $(td).html('' + cellData + ''); + } + } + }, + { + "targets": [6], + "data":"started", + "render": function ( data, type, full ) { + return moment(data, "X").format(time_format); + }, + "searchable": false + }, + { + "targets": [7], + "data":"paused_counter", + "render": function ( data, type, full ) { + return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins'; + }, + "searchable": false + }, + { + "targets": [8], + "data":"stopped", + "render": function ( data, type, full ) { + if (data !== null) { + return moment(data, "X").format(time_format); + } else { + return data; + } + }, + "searchable": false + }, + { + "targets": [9], + "data":"duration", + "render": function ( data, type, full ) { + if (data !== null) { + return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins'; + } else { + return data; + } + }, + "searchable": false + }, + { + "targets": [10], + "data":"percent_complete", + "orderable": false, + "render": function ( data, type, full ) { + if (data < 95) { + return ''+Math.round(data)+'%'; + } else { + return '100%'; + } + }, + "searchable": false + }, + { + "targets": [11], + "data":"rating_key", + "visible": false, + "searchable": false + }, + { + "targets": [12], + "data":"xml", + "searchable":false, + "visible":false + } + ], + "drawCallback": function (settings) { + // Jump to top of page + // $('html,body').scrollTop(0); + $('#ajaxMsg').addClass('success').fadeOut(); + }, + "preDrawCallback": function(settings) { + $('#ajaxMsg').html("
Fetching rows...
"); + $('#ajaxMsg').addClass('success').fadeIn(); + } +} + +$('#history_table').on('mouseenter', 'td.modal-control span', function () { + $(this).tooltip(); +}); + +$('#history_table').on('click', 'td.modal-control', function () { + var tr = $(this).parents('tr'); + var row = history_table.row( tr ); + var rowData = row.data(); + + function showStreamDetails() { + $.ajax({ + url: 'get_stream_data', + data: {row_id: rowData['id'], user: rowData['user']}, + cache: false, + async: true, + complete: function(xhr, status) { + $("#info-modal").html(xhr.responseText); + } + }); + } + showStreamDetails(); +}); \ No newline at end of file diff --git a/data/interfaces/default/stream_data.html b/data/interfaces/default/stream_data.html new file mode 100644 index 00000000..fd3d899f --- /dev/null +++ b/data/interfaces/default/stream_data.html @@ -0,0 +1,52 @@ +% if data is not None: + + + +% endif \ No newline at end of file diff --git a/data/interfaces/default/user.html b/data/interfaces/default/user.html index 3f000be9..46d36fdf 100644 --- a/data/interfaces/default/user.html +++ b/data/interfaces/default/user.html @@ -121,17 +121,76 @@
- Table - + + + + + + + + + + + + + + + + + + + + +
ID Time User Platform IP Address Title Started Paused Stopped Duration Completed RatingKey
+ @@ -146,7 +205,20 @@ + \ No newline at end of file diff --git a/plexpy/datatables.py b/plexpy/datatables.py index c78722a2..4d87ee32 100644 --- a/plexpy/datatables.py +++ b/plexpy/datatables.py @@ -28,6 +28,7 @@ class DataTables(object): def __init__(self): self.ssp_db = db.DBConnection() + # TODO: Pass all parameters via kwargs def ssp_query(self, table_name, columns=[], start=0, @@ -53,16 +54,26 @@ class DataTables(object): # TODO: custom_where is ugly and causes issues with reported total results if custom_where != '': - where += 'AND (' + custom_where + ')' + custom_where = 'WHERE (' + custom_where + ')' if grouping: - query = 'SELECT * FROM (SELECT %s FROM %s GROUP BY %s) %s %s' \ - % (column_data['column_string'], table_name, group_by, - where, order) + if custom_where == '': + query = 'SELECT * FROM (SELECT %s FROM %s GROUP BY %s) %s %s' \ + % (column_data['column_string'], table_name, group_by, + where, order) + else: + query = 'SELECT * FROM (SELECT * FROM (SELECT %s FROM %s GROUP BY %s) %s %s) %s' \ + % (column_data['column_string'], table_name, group_by, + where, order, custom_where) else: - query = 'SELECT %s FROM %s %s %s' \ - % (column_data['column_string'], table_name, where, - order) + if custom_where == '': + query = 'SELECT %s FROM %s %s %s' \ + % (column_data['column_string'], table_name, where, + order) + else: + query = 'SELECT * FROM (SELECT %s FROM %s %s %s) %s' \ + % (column_data['column_string'], table_name, where, + order, custom_where) filtered = self.ssp_db.select(query) if search_value == '': @@ -122,7 +133,9 @@ class DataTables(object): return where else: - return '' + where = '' + + return where @staticmethod def extract_columns(columns=[]): diff --git a/plexpy/plexwatch.py b/plexpy/plexwatch.py index 76790819..3f2d8aea 100644 --- a/plexpy/plexwatch.py +++ b/plexpy/plexwatch.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, helpers, request, datatables, config +from plexpy import logger, helpers, request, datatables, config, db from xml.dom import minidom import plexpy @@ -104,7 +104,7 @@ class PlexWatch(object): return dict - def get_history(self, start='', length='', kwargs=None): + def get_history(self, start='', length='', kwargs=None, custom_where=''): data_tables = datatables.DataTables() start = int(start) @@ -152,7 +152,7 @@ class PlexWatch(object): order_dir=order_dir, search_value=search_value, search_regex=search_regex, - custom_where='', + custom_where=custom_where, group_by='', kwargs=kwargs) @@ -216,3 +216,112 @@ class PlexWatch(object): } return dict + + def get_stream_details(self, id=0): + + myDB = db.DBConnection() + + query = 'SELECT xml from %s where id = %s' % (self.get_history_table_name(), id) + xml = myDB.select_single(query) + + try: + dict_data = helpers.convert_xml_to_dict(helpers.latinToAscii(xml)) + except IOError, e: + logger.warn("Error parsing XML in PlexWatch db: %s" % e) + + dict = {'id': id, + 'data': dict_data} + + return dict + + """ + Validate xml keys to make sure they exist and return their attribute value, return blank value is none found + """ + @staticmethod + def get_xml_attr(xml_key, attribute, return_bool=False, default_return=''): + if xml_key.getAttribute(attribute): + if return_bool: + return True + else: + return xml_key.getAttribute(attribute) + else: + if return_bool: + return False + else: + return default_return + + def get_stream_details(self, row_id=None): + myDB = db.DBConnection() + + if row_id: + query = 'SELECT xml from %s where id = %s' % (self.get_history_table_name(), row_id) + xml = myDB.select_single(query) + xml_data = helpers.latinToAscii(xml) + else: + return None + + try: + xml_parse = minidom.parseString(xml_data) + except: + logger.warn("Error parsing XML for Plex stream data.") + return None + + xml_head = xml_parse.getElementsByTagName('opt') + if not xml_head: + logger.warn("Error parsing XML for Plex stream data.") + return None + + stream_output = {} + + for a in xml_head: + media_type = self.get_xml_attr(a, 'type') + title = self.get_xml_attr(a, 'title') + grandparent_title = self.get_xml_attr(a, 'grandparentTitle') + + if a.getElementsByTagName('TranscodeSession'): + transcode_data = a.getElementsByTagName('TranscodeSession') + for transcode_session in transcode_data: + transcode_video_dec = self.get_xml_attr(transcode_session, 'videoDecision') + transcode_video_codec = self.get_xml_attr(transcode_session, 'videoCodec') + transcode_height = self.get_xml_attr(transcode_session, 'height') + transcode_width = self.get_xml_attr(transcode_session, 'width') + transcode_audio_dec = self.get_xml_attr(transcode_session, 'audioDecision') + transcode_audio_codec = self.get_xml_attr(transcode_session, 'audioCodec') + transcode_audio_channels = self.get_xml_attr(transcode_session, 'audioChannels') + else: + transcode_data = a.getElementsByTagName('Media') + for transcode_session in transcode_data: + transcode_video_dec = 'direct play' + transcode_video_codec = self.get_xml_attr(transcode_session, 'videoCodec') + transcode_height = self.get_xml_attr(transcode_session, 'height') + transcode_width = self.get_xml_attr(transcode_session, 'width') + transcode_audio_dec = 'direct play' + transcode_audio_codec = self.get_xml_attr(transcode_session, 'audioCodec') + transcode_audio_channels = self.get_xml_attr(transcode_session, 'audioChannels') + + if a.getElementsByTagName('Media'): + stream_data = a.getElementsByTagName('Media') + for stream_item in stream_data: + stream_output = {'container': self.get_xml_attr(stream_item, 'container'), + 'bitrate': self.get_xml_attr(stream_item, 'bitrate'), + 'video_resolution': self.get_xml_attr(stream_item, 'videoResolution'), + 'width': self.get_xml_attr(stream_item, 'width'), + 'height': self.get_xml_attr(stream_item, 'height'), + 'aspect_ratio': self.get_xml_attr(stream_item, 'aspectRatio'), + 'video_framerate': self.get_xml_attr(stream_item, 'videoFrameRate'), + 'video_codec': self.get_xml_attr(stream_item, 'videoCodec'), + 'audio_codec': self.get_xml_attr(stream_item, 'audioCodec'), + 'audio_channels': self.get_xml_attr(stream_item, 'audioChannels'), + 'transcode_video_dec': transcode_video_dec, + 'transcode_video_codec': transcode_video_codec, + 'transcode_height': transcode_height, + 'transcode_width': transcode_width, + 'transcode_audio_dec': transcode_audio_dec, + 'transcode_audio_codec': transcode_audio_codec, + 'transcode_audio_channels': transcode_audio_channels, + 'media_type': media_type, + 'title': title, + 'grandparent_title': grandparent_title + } + + return stream_output \ No newline at end of file diff --git a/plexpy/webserve.py b/plexpy/webserve.py index a577e399..37fb5c84 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, db, helpers, notifiers, plextv, pmsconnect, plexwatch +from plexpy import logger, helpers, notifiers, plextv, pmsconnect, plexwatch from plexpy.helpers import checked, radio, today, cleanName from xml.dom import minidom @@ -67,7 +67,7 @@ class WebInterface(object): return serve_template(templatename="index.html", title="Home") @cherrypy.expose - def history(self): + def get_date_formats(self): if plexpy.CONFIG.DATE_FORMAT: date_format = plexpy.CONFIG.DATE_FORMAT else: @@ -77,8 +77,15 @@ class WebInterface(object): else: time_format = 'HH:mm' - return serve_template(templatename="history.html", title="History", date_format=date_format, - time_format=time_format) + formats = {'date_format': date_format, + 'time_format': time_format} + + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(formats) + + @cherrypy.expose + def history(self): + return serve_template(templatename="history.html", title="History") @cherrypy.expose def users(self): @@ -88,6 +95,14 @@ class WebInterface(object): def user(self): return serve_template(templatename="user.html", title="User") + @cherrypy.expose + def get_stream_data(self, row_id=None, user='', **kwargs): + + plex_watch = plexwatch.PlexWatch() + stream_data = plex_watch.get_stream_details(row_id) + + return serve_template(templatename="stream_data.html", title="Stream Data", data=stream_data, user=user) + @cherrypy.expose def get_user_list(self, start=0, length=100, **kwargs): @@ -332,34 +347,29 @@ class WebInterface(object): message=message, timer=timer) @cherrypy.expose - def getHistory_json(self, start=0, length=100, **kwargs): + def get_history(self, start=0, length=100, custom_where='', **kwargs): + + if 'user' in kwargs: + user = kwargs.get('user', "") + custom_where = 'user = "%s"' % user + if 'rating_key' in kwargs: + rating_key = kwargs.get('rating_key', "") + custom_where = 'rating_key = %s' % rating_key plex_watch = plexwatch.PlexWatch() - history = plex_watch.get_history(start, length, kwargs) + history = plex_watch.get_history(start, length, kwargs, custom_where) cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps(history) @cherrypy.expose - def getStreamDetails(self, id=0, **kwargs): + def get_stream_details(self, rating_key=0, **kwargs): - myDB = db.DBConnection() - db_table = db.DBConnection().get_history_table_name() + plex_watch = plexwatch.PlexWatch() + stream_details = plex_watch.get_stream_details(rating_key) - query = 'SELECT xml from %s where id = %s' % (db_table, id) - xml = myDB.select_single(query) - - try: - dict_data = helpers.convert_xml_to_dict(helpers.latinToAscii(xml)) - except IOError, e: - logger.warn("Error parsing XML in PlexWatch db: %s" % e) - - dict = {'id': id, - 'data': dict_data} - - s = json.dumps(dict) cherrypy.response.headers['Content-type'] = 'application/json' - return s + return json.dumps(stream_details) @cherrypy.expose @@ -554,6 +564,18 @@ class WebInterface(object): pms_connect = pmsconnect.PmsConnect() result = pms_connect.get_recently_added(count, 'json') + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return result + else: + logger.warn('Unable to retrieve data.') + + @cherrypy.expose + def get_stream(self, row_id='', **kwargs): + + plex_watch = plexwatch.PlexWatch() + result = plex_watch.get_stream_details('122') + if result: cherrypy.response.headers['Content-type'] = 'application/json' return result