From d216c0d76a52fd0e5121ca53bacf9bb4351b2051 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 15 Jul 2015 12:50:33 +0200 Subject: [PATCH] Enable video logging by default. Move database functions to its own file. Incorporate datatables changes into original file and remove the test file. Remove old plexWatch processing and db files and references. --- data/interfaces/default/config.html | 28 +- plexpy/__init__.py | 3 - plexpy/config.py | 2 +- plexpy/database.py | 129 ++++ plexpy/datafactory.py | 34 +- plexpy/datatables.py | 27 +- plexpy/datatables_new.py | 222 ------ plexpy/db.py | 121 --- plexpy/monitor.py | 116 +-- plexpy/plextv.py | 10 +- plexpy/plexwatch.py | 1069 --------------------------- plexpy/pmsconnect.py | 8 +- plexpy/webserve.py | 19 +- 13 files changed, 196 insertions(+), 1592 deletions(-) create mode 100644 plexpy/database.py delete mode 100644 plexpy/datatables_new.py delete mode 100644 plexpy/db.py delete mode 100644 plexpy/plexwatch.py diff --git a/data/interfaces/default/config.html b/data/interfaces/default/config.html index 33bac8de..412092db 100644 --- a/data/interfaces/default/config.html +++ b/data/interfaces/default/config.html @@ -146,16 +146,6 @@

Set your preferred time format. Click here to see the parameter list.

-
-
-

Plex Logs

-
-
- - -

Set the folder where your Plex Server logs are.
Click here for help.

-
-

@@ -175,8 +165,8 @@
- -

Token for Plex.tv authentication. Leave empty if no authentication is required.

+ +

Token for Plex.tv authentication. This field is required.

Fetch Token

@@ -190,6 +180,17 @@ Use BIF thumbs

If you have media indexing enabled on your server, use these on the activity pane.

+ +
+

Plex Logs

+
+
+
+ + +

Set the folder where your Plex Server logs are.
Click here for help.

+
+
- @@ -262,7 +262,7 @@
Log Music -

Keep records of all audio items played from your Plex Media Server. Experimental.

+

Keep records of all audio items played from your Plex Media Server. VERY experimental.

diff --git a/plexpy/__init__.py b/plexpy/__init__.py index 53dff28b..ce658cff 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -13,9 +13,6 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -# NZBGet support added by CurlyMo as a part of -# XBian - XBMC on the Raspberry Pi - import os import sys import subprocess diff --git a/plexpy/config.py b/plexpy/config.py index 366fd422..07753917 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -117,7 +117,7 @@ _CONFIG_DEFINITIONS = { 'TWITTER_USERNAME': (str, 'Twitter', ''), 'UPDATE_DB_INTERVAL': (int, 'General', 24), 'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1), - 'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 0), + 'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1), 'XBMC_ENABLED': (int, 'XBMC', 0), 'XBMC_HOST': (str, 'XBMC', ''), 'XBMC_PASSWORD': (str, 'XBMC', ''), diff --git a/plexpy/database.py b/plexpy/database.py new file mode 100644 index 00000000..59bb2e10 --- /dev/null +++ b/plexpy/database.py @@ -0,0 +1,129 @@ +# This file is part of PlexPy. +# +# PlexPy is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# PlexPy is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with PlexPy. If not, see . + +from plexpy import logger + +import sqlite3 +import os +import plexpy + + +def drop_session_db(): + monitor_db = MonitorDatabase() + monitor_db.action('DROP TABLE sessions') + +def clear_history_tables(): + logger.debug(u"PlexPy Database :: Deleting all session_history records... No turning back now bub.") + monitor_db = MonitorDatabase() + monitor_db.action('DELETE FROM session_history') + monitor_db.action('DELETE FROM session_history_media_info') + monitor_db.action('DELETE FROM session_history_metadata') + monitor_db.action('VACUUM;') + +def db_filename(filename="plexpy.db"): + + return os.path.join(plexpy.DATA_DIR, filename) + +def get_cache_size(): + # This will protect against typecasting problems produced by empty string and None settings + if not plexpy.CONFIG.CACHE_SIZEMB: + # sqlite will work with this (very slowly) + return 0 + return int(plexpy.CONFIG.CACHE_SIZEMB) + + +class MonitorDatabase(object): + + def __init__(self, filename='plexpy.db'): + self.filename = filename + self.connection = sqlite3.connect(db_filename(filename), timeout=20) + # Don't wait for the disk to finish writing + self.connection.execute("PRAGMA synchronous = OFF") + # Journal disabled since we never do rollbacks + self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE) + # 64mb of cache memory, probably need to make it user configurable + self.connection.execute("PRAGMA cache_size=-%s" % (get_cache_size() * 1024)) + self.connection.row_factory = sqlite3.Row + + def action(self, query, args=None, return_last_id=False): + + if query is None: + return + + sql_result = None + + try: + with self.connection as c: + if args is None: + sql_result = c.execute(query) + else: + sql_result = c.execute(query, args) + + except sqlite3.OperationalError, e: + if "unable to open database file" in e.message or "database is locked" in e.message: + logger.warn('Database Error: %s', e) + else: + logger.error('Database error: %s', e) + raise + + except sqlite3.DatabaseError, e: + logger.error('Fatal Error executing %s :: %s', query, e) + raise + + return sql_result + + def select(self, query, args=None): + + sql_results = self.action(query, args).fetchall() + + if sql_results is None or sql_results == [None]: + return [] + + return sql_results + + def select_single(self, query, args=None): + + sql_results = self.action(query, args).fetchone()[0] + + if sql_results is None or sql_results == "": + return "" + + return sql_results + + def upsert(self, table_name, value_dict, key_dict): + + trans_type = 'update' + changes_before = self.connection.total_changes + + gen_params = lambda my_dict: [x + " = ?" for x in my_dict.keys()] + + update_query = "UPDATE " + table_name + " SET " + ", ".join(gen_params(value_dict)) + \ + " WHERE " + " AND ".join(gen_params(key_dict)) + + self.action(update_query, value_dict.values() + key_dict.values()) + + if self.connection.total_changes == changes_before: + trans_type = 'insert' + insert_query = ( + "INSERT INTO " + table_name + " (" + ", ".join(value_dict.keys() + key_dict.keys()) + ")" + + " VALUES (" + ", ".join(["?"] * len(value_dict.keys() + key_dict.keys())) + ")" + ) + try: + self.action(insert_query, value_dict.values() + key_dict.values()) + except sqlite3.IntegrityError: + logger.info('Queries failed: %s and %s', update_query, insert_query) + + # We want to know if it was an update or insert + return trans_type diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index a155a92c..796d986c 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -13,11 +13,9 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, helpers, datatables_new, common, monitor -from xml.dom import minidom +from plexpy import logger, datatables, common, database import datetime -import plexpy class DataFactory(object): @@ -29,7 +27,7 @@ class DataFactory(object): pass def get_user_list(self, start='', length='', kwargs=None): - data_tables = datatables_new.DataTables() + data_tables = datatables.DataTables() start = int(start) length = int(length) @@ -114,7 +112,7 @@ class DataFactory(object): return dict def get_history(self, start='', length='', kwargs=None, custom_where=''): - data_tables = datatables_new.DataTables() + data_tables = datatables.DataTables() start = int(start) length = int(length) @@ -234,7 +232,7 @@ class DataFactory(object): return dict def get_user_unique_ips(self, start='', length='', kwargs=None, custom_where=''): - data_tables = datatables_new.DataTables() + data_tables = datatables.DataTables() start = int(start) length = int(length) @@ -311,7 +309,7 @@ class DataFactory(object): if friendly_name.strip() == '': friendly_name = None - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() control_value_dict = {"username": user} new_value_dict = {"friendly_name": friendly_name} @@ -323,7 +321,7 @@ class DataFactory(object): def get_user_friendly_name(self, user=None): if user: try: - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() query = 'select friendly_name FROM users WHERE username = ?' result = monitor_db.select_single(query, args=[user]) if result: @@ -338,7 +336,7 @@ class DataFactory(object): def get_user_id(self, user=None): if user: try: - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() query = 'select user_id FROM users WHERE username = ?' result = monitor_db.select_single(query, args=[user]) if result: @@ -352,7 +350,7 @@ class DataFactory(object): def get_user_details(self, user=None, user_id=None): try: - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if user: query = 'SELECT user_id, username, friendly_name, email, ' \ @@ -399,7 +397,7 @@ class DataFactory(object): return None def get_home_stats(self, time_range='30'): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if not time_range.isdigit(): time_range = '30' @@ -568,7 +566,7 @@ class DataFactory(object): return home_stats def get_stream_details(self, row_id=None): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if row_id: query = 'SELECT container, bitrate, video_resolution, width, height, aspect_ratio, video_framerate, ' \ @@ -611,7 +609,7 @@ class DataFactory(object): return stream_output def get_recently_watched(self, user=None, limit='10'): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() recently_watched = [] if not limit.isdigit(): @@ -657,7 +655,7 @@ class DataFactory(object): return recently_watched def get_user_watch_time_stats(self, user=None): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() time_queries = [1, 7, 30, 0] user_watch_time_stats = [] @@ -697,7 +695,7 @@ class DataFactory(object): return user_watch_time_stats def get_user_platform_stats(self, user=None): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() platform_stats = [] result_id = 0 @@ -725,7 +723,7 @@ class DataFactory(object): return platform_stats def get_total_plays_per_day(self, time_range='30'): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if not time_range.isdigit(): time_range = '30' @@ -780,7 +778,7 @@ class DataFactory(object): return output def get_total_plays_per_dayofweek(self, time_range='30'): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if not time_range.isdigit(): time_range = '30' @@ -829,7 +827,7 @@ class DataFactory(object): return output def get_total_plays_per_hourofday(self, time_range='30'): - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if not time_range.isdigit(): time_range = '30' diff --git a/plexpy/datatables.py b/plexpy/datatables.py index 4c14a51f..912c0b6a 100644 --- a/plexpy/datatables.py +++ b/plexpy/datatables.py @@ -15,7 +15,7 @@ # TODO: Implement with sqlite3 directly instead of using db class -from plexpy import logger, helpers, db +from plexpy import logger, helpers, database import re @@ -26,7 +26,8 @@ class DataTables(object): """ def __init__(self): - self.ssp_db = db.DBConnection() + self.ssp_db = database.MonitorDatabase() + logger.debug(u"Database initilised!") # TODO: Pass all parameters via kwargs def ssp_query(self, table_name, @@ -57,12 +58,20 @@ class DataTables(object): join = '' if join_type: - if join_type.upper() == 'LEFT OUTER JOIN': - join = 'LEFT OUTER JOIN %s ON %s = %s' % (join_table, join_evals[0], join_evals[1]) - elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN': - join = 'INNER JOIN %s ON %s = %s' % (join_table, join_evals[0], join_evals[1]) - else: - join = '' + join_iter = 0 + for join_type_item in join_type: + if join_type_item.upper() == 'LEFT OUTER JOIN': + join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \ + (join_table[join_iter], join_evals[join_iter][0], join_evals[join_iter][1]) + elif join_type_item.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN': + join_item = 'INNER JOIN %s ON %s = %s ' % \ + (join_table[join_iter], join_evals[join_iter][0], join_evals[join_iter][1]) + else: + join_item = '' + join_iter += 1 + join += join_item + + logger.debug(u"join string = %s" % join) # TODO: custom_where is ugly and causes issues with reported total results if custom_where != '': @@ -87,7 +96,7 @@ class DataTables(object): % (column_data['column_string'], table_name, join, where, order, custom_where) - # logger.debug(u"Query string: %s" % query) + logger.debug(u"Query string: %s" % query) filtered = self.ssp_db.select(query) if search_value == '': diff --git a/plexpy/datatables_new.py b/plexpy/datatables_new.py deleted file mode 100644 index f2a96ed3..00000000 --- a/plexpy/datatables_new.py +++ /dev/null @@ -1,222 +0,0 @@ -# This file is part of PlexPy. -# -# PlexPy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PlexPy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PlexPy. If not, see . - -# TODO: Implement with sqlite3 directly instead of using db class - -from plexpy import logger, helpers, monitor - -import re - - -class DataTables(object): - """ - Server side processing for Datatables - """ - - def __init__(self): - self.ssp_db = monitor.MonitorDatabase() - logger.debug(u"Database initilised!") - - # TODO: Pass all parameters via kwargs - def ssp_query(self, table_name, - columns=[], - start=0, - length=0, - order_column=0, - order_dir='asc', - search_value='', - search_regex='', - custom_where='', - group_by='', - join_type=None, - join_table=None, - join_evals=None, - kwargs=None): - - parameters = self.process_kwargs(kwargs) - - if group_by != '': - grouping = True - else: - grouping = False - - column_data = self.extract_columns(columns) - where = self.construct_where(column_data, search_value, grouping, parameters) - order = self.construct_order(column_data, order_column, order_dir, parameters, table_name, grouping) - join = '' - - if join_type: - join_iter = 0 - for join_type_item in join_type: - if join_type_item.upper() == 'LEFT OUTER JOIN': - join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \ - (join_table[join_iter], join_evals[join_iter][0], join_evals[join_iter][1]) - elif join_type_item.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN': - join_item = 'INNER JOIN %s ON %s = %s ' % \ - (join_table[join_iter], join_evals[join_iter][0], join_evals[join_iter][1]) - else: - join_item = '' - join_iter += 1 - join += join_item - - logger.debug(u"join string = %s" % join) - - # TODO: custom_where is ugly and causes issues with reported total results - if custom_where != '': - custom_where = 'WHERE (' + custom_where + ')' - - if grouping: - if custom_where == '': - query = 'SELECT * FROM (SELECT %s FROM %s %s GROUP BY %s) %s %s' \ - % (column_data['column_string'], table_name, join, group_by, - where, order) - else: - query = 'SELECT * FROM (SELECT * FROM (SELECT %s FROM %s %s GROUP BY %s) %s %s) %s' \ - % (column_data['column_string'], table_name, join, group_by, - where, order, custom_where) - else: - if custom_where == '': - query = 'SELECT %s FROM %s %s %s %s' \ - % (column_data['column_string'], table_name, join, where, - order) - else: - query = 'SELECT * FROM (SELECT %s FROM %s %s %s %s) %s' \ - % (column_data['column_string'], table_name, join, where, - order, custom_where) - - logger.debug(u"Query string: %s" % query) - filtered = self.ssp_db.select(query) - - if search_value == '': - totalcount = len(filtered) - else: - totalcount = self.ssp_db.select('SELECT COUNT(*) from %s' % table_name)[0][0] - - result = filtered[start:(start + length)] - output = {'result': result, - 'filteredCount': len(filtered), - 'totalCount': totalcount} - - return output - - @staticmethod - def construct_order(column_data, order_column, order_dir, parameters=None, table_name=None, grouped=False): - order = '' - if grouped: - sort_col = column_data['column_named'][order_column] - else: - sort_col = column_data['column_order'][order_column] - if parameters: - for parameter in parameters: - if parameter['data'] != '': - if int(order_column) == parameter['index']: - if parameter['data'] in column_data['column_named'] and parameter['orderable'] == 'true': - if table_name and table_name != '': - order = 'ORDER BY %s COLLATE NOCASE %s' % (sort_col, order_dir) - else: - order = 'ORDER BY %s COLLATE NOCASE %s' % (sort_col, order_dir) - else: - order = 'ORDER BY %s COLLATE NOCASE %s' % (sort_col, order_dir) - - return order - - @staticmethod - def construct_where(column_data, search_value='', grouping=False, parameters=None): - if search_value != '': - where = 'WHERE ' - if parameters: - for column in column_data['column_named']: - search_skip = False - for parameter in parameters: - if column.rpartition('.')[-1] in parameter['data']: - if parameter['searchable'] == 'true': - where += column + ' LIKE "%' + search_value + '%" OR ' - search_skip = True - else: - search_skip = True - - if not search_skip: - where += column + ' LIKE "%' + search_value + '%" OR ' - else: - for column in column_data['column_named']: - where += column + ' LIKE "%' + search_value + '%" OR ' - - # TODO: This will break the query if all parameters are excluded - where = where[:-4] - - return where - else: - where = '' - - return where - - @staticmethod - def extract_columns(columns=[]): - columns_string = '' - columns_literal = [] - columns_named = [] - columns_order = [] - - for column in columns: - columns_string += column - columns_string += ', ' - # TODO: make this case insensitive - if ' as ' in column: - columns_literal.append(column.rpartition(' as ')[0]) - columns_named.append(column.rpartition(' as ')[-1].rpartition('.')[-1]) - columns_order.append(column.rpartition(' as ')[-1]) - else: - columns_literal.append(column) - columns_named.append(column.rpartition('.')[-1]) - columns_order.append(column) - - columns_string = columns_string[:-2] - - column_data = {'column_string': columns_string, - 'column_literal': columns_literal, - 'column_named': columns_named, - 'column_order': columns_order - } - - return column_data - - # TODO: Fix this method. Should not break if kwarg list is not sorted. - def process_kwargs(self, kwargs): - - column_parameters = [] - - for kwarg in sorted(kwargs): - if re.search(r"\[(\w+)\]", kwarg) and kwarg[:7] == 'columns': - parameters = re.findall(r"\[(\w+)\]", kwarg) - array_index = '' - for parameter in parameters: - pass_complete = False - if parameter.isdigit(): - array_index = parameter - if parameter == 'data': - data = kwargs.get('columns[' + array_index + '][data]', "") - if parameter == 'orderable': - orderable = kwargs.get('columns[' + array_index + '][orderable]', "") - if parameter == 'searchable': - searchable = kwargs.get('columns[' + array_index + '][searchable]', "") - pass_complete = True - if pass_complete: - row = {'index': int(array_index), - 'data': data, - 'searchable': searchable, - 'orderable': orderable} - column_parameters.append(row) - - return sorted(column_parameters, key=lambda i: i['index']) \ No newline at end of file diff --git a/plexpy/db.py b/plexpy/db.py deleted file mode 100644 index 34fcb2f1..00000000 --- a/plexpy/db.py +++ /dev/null @@ -1,121 +0,0 @@ -# This file is part of PlexPy. -# -# PlexPy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PlexPy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PlexPy. If not, see . - -##################################### -## Stolen from Sick-Beard's db.py ## -##################################### - -from __future__ import with_statement - -import os -import sqlite3 - -import plexpy - -from plexpy import logger - - -def dbFilename(filename): - - return os.path.join(plexpy.DATA_DIR, filename) - - -def getCacheSize(): - #this will protect against typecasting problems produced by empty string and None settings - if not plexpy.CONFIG.CACHE_SIZEMB: - #sqlite will work with this (very slowly) - return 0 - return int(plexpy.CONFIG.CACHE_SIZEMB) - - -class DBConnection: - - def __init__(self): - - self.filename = plexpy.CONFIG.PLEXWATCH_DATABASE - #self.connection = sqlite3.connect(dbFilename(plexpy.CONFIG.PLEXWATCH_DATABASE), timeout=20) - self.connection = sqlite3.connect(plexpy.CONFIG.PLEXWATCH_DATABASE, timeout=20) - #don't wait for the disk to finish writing - self.connection.execute("PRAGMA synchronous = OFF") - #journal disabled since we never do rollbacks - self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE) - #64mb of cache memory,probably need to make it user configurable - self.connection.execute("PRAGMA cache_size=-%s" % (getCacheSize() * 1024)) - self.connection.row_factory = sqlite3.Row - - def action(self, query, args=None): - - if query is None: - return - - sqlResult = None - - try: - with self.connection as c: - if args is None: - sqlResult = c.execute(query) - else: - sqlResult = c.execute(query, args) - - except sqlite3.OperationalError, e: - if "unable to open database file" in e.message or "database is locked" in e.message: - logger.warn('Database Error: %s', e) - else: - logger.error('Database error: %s', e) - raise - - except sqlite3.DatabaseError, e: - logger.error('Fatal Error executing %s :: %s', query, e) - raise - - return sqlResult - - def select(self, query, args=None): - - sqlResults = self.action(query, args).fetchall() - - if sqlResults is None or sqlResults == [None]: - return [] - - return sqlResults - - def select_single(self, query, args=None): - - sqlResult = self.action(query, args).fetchone()[0] - - if sqlResult is None or sqlResult == "": - return "" - - return sqlResult - - def upsert(self, tableName, valueDict, keyDict): - - changesBefore = self.connection.total_changes - - genParams = lambda myDict: [x + " = ?" for x in myDict.keys()] - - update_query = "UPDATE " + tableName + " SET " + ", ".join(genParams(valueDict)) + " WHERE " + " AND ".join(genParams(keyDict)) - - self.action(update_query, valueDict.values() + keyDict.values()) - - if self.connection.total_changes == changesBefore: - insert_query = ( - "INSERT INTO " + tableName + " (" + ", ".join(valueDict.keys() + keyDict.keys()) + ")" + - " VALUES (" + ", ".join(["?"] * len(valueDict.keys() + keyDict.keys())) + ")" - ) - try: - self.action(insert_query, valueDict.values() + keyDict.values()) - except sqlite3.IntegrityError: - logger.info('Queries failed: %s and %s', update_query, insert_query) diff --git a/plexpy/monitor.py b/plexpy/monitor.py index fb31b644..f074ba49 100644 --- a/plexpy/monitor.py +++ b/plexpy/monitor.py @@ -13,10 +13,8 @@ # You should have received a copy of the GNU General Public License # along with PlexPy. If not, see . -from plexpy import logger, helpers, pmsconnect, notification_handler, config, log_reader, common +from plexpy import logger, pmsconnect, notification_handler, log_reader, common, database -import os -import sqlite3 import threading import plexpy import re @@ -29,7 +27,7 @@ def check_active_sessions(): with monitor_lock: pms_connect = pmsconnect.PmsConnect() session_list = pms_connect.get_current_activity() - monitor_db = MonitorDatabase() + monitor_db = database.MonitorDatabase() monitor_process = MonitorProcessing() # logger.debug(u"PlexPy Monitor :: Checking for active streams.") @@ -126,119 +124,11 @@ def check_active_sessions(): else: logger.debug(u"PlexPy Monitor :: Unable to read session list.") -def drop_session_db(): - monitor_db = MonitorDatabase() - monitor_db.action('DROP TABLE sessions') - -def clear_history_tables(): - logger.debug(u"PlexPy Monitor :: Deleting all session_history records... No turning back now bub.") - monitor_db = MonitorDatabase() - monitor_db.action('DELETE FROM session_history') - monitor_db.action('DELETE FROM session_history_media_info') - monitor_db.action('DELETE FROM session_history_metadata') - monitor_db.action('VACUUM;') - -def db_filename(filename="plexpy.db"): - - return os.path.join(plexpy.DATA_DIR, filename) - -def get_cache_size(): - # This will protect against typecasting problems produced by empty string and None settings - if not plexpy.CONFIG.CACHE_SIZEMB: - # sqlite will work with this (very slowly) - return 0 - return int(plexpy.CONFIG.CACHE_SIZEMB) - - -class MonitorDatabase(object): - - def __init__(self, filename='plexpy.db'): - self.filename = filename - self.connection = sqlite3.connect(db_filename(filename), timeout=20) - # Don't wait for the disk to finish writing - self.connection.execute("PRAGMA synchronous = OFF") - # Journal disabled since we never do rollbacks - self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE) - # 64mb of cache memory, probably need to make it user configurable - self.connection.execute("PRAGMA cache_size=-%s" % (get_cache_size() * 1024)) - self.connection.row_factory = sqlite3.Row - - def action(self, query, args=None, return_last_id=False): - - if query is None: - return - - sql_result = None - - try: - with self.connection as c: - if args is None: - sql_result = c.execute(query) - else: - sql_result = c.execute(query, args) - - except sqlite3.OperationalError, e: - if "unable to open database file" in e.message or "database is locked" in e.message: - logger.warn('Database Error: %s', e) - else: - logger.error('Database error: %s', e) - raise - - except sqlite3.DatabaseError, e: - logger.error('Fatal Error executing %s :: %s', query, e) - raise - - return sql_result - - def select(self, query, args=None): - - sql_results = self.action(query, args).fetchall() - - if sql_results is None or sql_results == [None]: - return [] - - return sql_results - - def select_single(self, query, args=None): - - sql_results = self.action(query, args).fetchone()[0] - - if sql_results is None or sql_results == "": - return "" - - return sql_results - - def upsert(self, table_name, value_dict, key_dict): - - trans_type = 'update' - changes_before = self.connection.total_changes - - gen_params = lambda my_dict: [x + " = ?" for x in my_dict.keys()] - - update_query = "UPDATE " + table_name + " SET " + ", ".join(gen_params(value_dict)) + \ - " WHERE " + " AND ".join(gen_params(key_dict)) - - self.action(update_query, value_dict.values() + key_dict.values()) - - if self.connection.total_changes == changes_before: - trans_type = 'insert' - insert_query = ( - "INSERT INTO " + table_name + " (" + ", ".join(value_dict.keys() + key_dict.keys()) + ")" + - " VALUES (" + ", ".join(["?"] * len(value_dict.keys() + key_dict.keys())) + ")" - ) - try: - self.action(insert_query, value_dict.values() + key_dict.values()) - except sqlite3.IntegrityError: - logger.info('Queries failed: %s and %s', update_query, insert_query) - - # We want to know if it was an update or insert - return trans_type - class MonitorProcessing(object): def __init__(self): - self.db = MonitorDatabase() + self.db = database.MonitorDatabase() def write_session(self, session=None): diff --git a/plexpy/plextv.py b/plexpy/plextv.py index 62458bc1..6469e934 100644 --- a/plexpy/plextv.py +++ b/plexpy/plextv.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, plexwatch, db, http_handler, monitor +from plexpy import logger, helpers, datafactory, http_handler, database from xml.dom import minidom @@ -23,7 +23,7 @@ import plexpy def refresh_users(): logger.info("Requesting users list refresh...") result = PlexTV().get_full_users_list() - monitor_db = monitor.MonitorDatabase() + monitor_db = database.MonitorDatabase() if len(result) > 0: for item in result: @@ -194,7 +194,7 @@ class PlexTV(object): def get_synced_items(self, machine_id=None, user_id=None): sync_list = self.get_plextv_sync_lists(machine_id) - plex_watch = plexwatch.PlexWatch() + data_factory = datafactory.DataFactory() synced_items = [] @@ -218,8 +218,8 @@ class PlexTV(object): for device in sync_device: device_user_id = helpers.get_xml_attr(device, 'userID') try: - device_username = plex_watch.get_user_details(user_id=device_user_id)['username'] - device_friendly_name = plex_watch.get_user_details(user_id=device_user_id)['friendly_name'] + device_username = data_factory.get_user_details(user_id=device_user_id)['username'] + device_friendly_name = data_factory.get_user_details(user_id=device_user_id)['friendly_name'] except: device_username = '' device_friendly_name = '' diff --git a/plexpy/plexwatch.py b/plexpy/plexwatch.py deleted file mode 100644 index 2d5d5af4..00000000 --- a/plexpy/plexwatch.py +++ /dev/null @@ -1,1069 +0,0 @@ -# This file is part of PlexPy. -# -# PlexPy is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# PlexPy is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with PlexPy. If not, see . - -from plexpy import logger, helpers, datatables, db, common -from xml.dom import minidom -import sys -if sys.version_info < (2, 7): - from backport_collections import defaultdict, Counter -else: - from collections import defaultdict, Counter -import datetime -import plexpy - - -class PlexWatch(object): - """ - Retrieve and process data from the plexwatch database - """ - - def __init__(self): - pass - - @staticmethod - def get_history_table_name(): - - if plexpy.CONFIG.GROUPING_GLOBAL_HISTORY: - return "grouped" - else: - return "processed" - - def get_user_list(self, start='', length='', kwargs=None): - data_tables = datatables.DataTables() - - start = int(start) - length = int(length) - filtered = [] - totalcount = 0 - search_value = "" - search_regex = "" - order_column = 1 - order_dir = "desc" - - if 'order[0][dir]' in kwargs: - order_dir = kwargs.get('order[0][dir]', "desc") - - if 'order[0][column]' in kwargs: - order_column = kwargs.get('order[0][column]', 1) - - if 'search[value]' in kwargs: - search_value = kwargs.get('search[value]', "") - - if 'search[regex]' in kwargs: - search_regex = kwargs.get('search[regex]', "") - - t = self.get_history_table_name() - - columns = [t + '.id', - '(case when plexpy_users.friendly_name is null then ' + t + - '.user else plexpy_users.friendly_name end) as friendly_name', - t + '.time', - t + '.ip_address', - 'COUNT(' + t + '.title) as plays', - t + '.user', - 'plexpy_users.user_id as user_id', - 'plexpy_users.thumb as thumb'] - try: - query = data_tables.ssp_query(table_name=t, - columns=columns, - start=start, - length=length, - order_column=int(order_column), - order_dir=order_dir, - search_value=search_value, - search_regex=search_regex, - custom_where='', - group_by=(t + '.user'), - join_type='LEFT OUTER JOIN', - join_table='plexpy_users', - join_evals=[t + '.user', 'plexpy_users.username'], - kwargs=kwargs) - except: - logger.warn("Unable to open PlexWatch database.") - return {'recordsFiltered': 0, - 'recordsTotal': 0, - 'data': 'null'}, - - users = query['result'] - - rows = [] - for item in users: - if not item['thumb'] or item['thumb'] == '': - user_thumb = common.DEFAULT_USER_THUMB - else: - user_thumb = item['thumb'] - - row = {"plays": item['plays'], - "time": item['time'], - "friendly_name": item["friendly_name"], - "ip_address": item["ip_address"], - "thumb": user_thumb, - "user": item["user"], - "user_id": item['user_id'] - } - - rows.append(row) - - dict = {'recordsFiltered': query['filteredCount'], - 'recordsTotal': query['totalCount'], - 'data': rows, - } - - return dict - - def get_user_unique_ips(self, start='', length='', kwargs=None, custom_where=''): - data_tables = datatables.DataTables() - - start = int(start) - length = int(length) - filtered = [] - totalcount = 0 - search_value = "" - search_regex = "" - order_column = 0 - order_dir = "desc" - - if 'order[0][dir]' in kwargs: - order_dir = kwargs.get('order[0][dir]', "desc") - - if 'order[0][column]' in kwargs: - order_column = kwargs.get('order[0][column]', 1) - - if 'search[value]' in kwargs: - search_value = kwargs.get('search[value]', "") - - if 'search[regex]' in kwargs: - search_regex = kwargs.get('search[regex]', "") - - t = self.get_history_table_name() - - columns = [t + '.time as last_seen', - t + '.user', - t + '.ip_address', - 'COUNT(' + t + '.ip_address) as play_count', - t + '.platform', - t + '.title as last_watched' - ] - - try: - query = data_tables.ssp_query(table_name=self.get_history_table_name(), - columns=columns, - start=start, - length=length, - order_column=int(order_column), - order_dir=order_dir, - search_value=search_value, - search_regex=search_regex, - custom_where=custom_where, - group_by=(t + '.ip_address'), - join_type=None, - join_table=None, - join_evals=None, - kwargs=kwargs) - except: - logger.warn("Unable to open PlexWatch database.") - return {'recordsFiltered': 0, - 'recordsTotal': 0, - 'data': 'null'}, - - results = query['result'] - - rows = [] - for item in results: - row = {"last_seen": item['last_seen'], - "ip_address": item['ip_address'], - "play_count": item['play_count'], - "platform": item['platform'], - "last_watched": item['last_watched'] - } - - rows.append(row) - - dict = {'recordsFiltered': query['filteredCount'], - 'recordsTotal': query['totalCount'], - 'data': rows, - } - - return dict - - def get_history(self, start='', length='', kwargs=None, custom_where=''): - data_tables = datatables.DataTables() - - start = int(start) - length = int(length) - filtered = [] - totalcount = 0 - search_value = "" - search_regex = "" - order_column = 1 - order_dir = "desc" - - t = self.get_history_table_name() - - if 'order[0][dir]' in kwargs: - order_dir = kwargs.get('order[0][dir]', "desc") - - if 'order[0][column]' in kwargs: - order_column = kwargs.get('order[0][column]', "1") - - if 'search[value]' in kwargs: - search_value = kwargs.get('search[value]', "") - - if 'search[regex]' in kwargs: - search_regex = kwargs.get('search[regex]', "") - - columns = [t + '.id', - t + '.time as date', - '(case when plexpy_users.friendly_name is null then ' + t + - '.user else plexpy_users.friendly_name end) as friendly_name', - t + '.platform', - t + '.ip_address', - t + '.title', - t + '.time as started', - t + '.paused_counter', - t + '.stopped', - 'round((julianday(datetime(' + t + '.stopped, "unixepoch", "localtime")) - \ - julianday(datetime(' + t + '.time, "unixepoch", "localtime"))) * 86400) - \ - (case when ' + t + '.paused_counter is null then 0 else ' + t + '.paused_counter end) as duration', - t + '.ratingKey as rating_key', - t + '.xml', - t + '.user', - t + '.grandparentRatingKey as grandparent_rating_key' - ] - try: - query = data_tables.ssp_query(table_name=t, - columns=columns, - start=start, - length=length, - order_column=int(order_column), - order_dir=order_dir, - search_value=search_value, - search_regex=search_regex, - custom_where=custom_where, - group_by='', - join_type='LEFT OUTER JOIN', - join_table='plexpy_users', - join_evals=[t + '.user', 'plexpy_users.username'], - kwargs=kwargs) - except: - logger.warn("Unable to open PlexWatch database.") - return {'recordsFiltered': 0, - 'recordsTotal': 0, - 'data': 'null'}, - - history = query['result'] - - rows = [] - # NOTE: We are adding in a blank xml field in order enable the Datatables "searchable" parameter - for item in history: - row = {"id": item['id'], - "date": item['date'], - "friendly_name": item['friendly_name'], - "platform": item["platform"], - "ip_address": item["ip_address"], - "title": item["title"], - "started": item["started"], - "paused_counter": item["paused_counter"], - "stopped": item["stopped"], - "rating_key": item["rating_key"], - "duration": item["duration"], - "percent_complete": 0, - "xml": "", - "user": item["user"] - } - - if item['paused_counter'] > 0: - row['paused_counter'] = item['paused_counter'] - else: - row['paused_counter'] = 0 - - if item['started']: - if item['stopped'] > 0: - stopped = item['stopped'] - else: - stopped = 0 - if item['paused_counter'] > 0: - paused_counter = item['paused_counter'] - else: - paused_counter = 0 - - try: - xml_parse = minidom.parseString(helpers.latinToAscii(item['xml'])) - except: - logger.warn("Error parsing XML in PlexWatch db") - - xml_head = xml_parse.getElementsByTagName('opt') - if not xml_head: - logger.warn("Error parsing XML in PlexWatch db.") - - for s in xml_head: - if s.getAttribute('duration') and s.getAttribute('viewOffset'): - view_offset = helpers.cast_to_float(s.getAttribute('viewOffset')) - duration = helpers.cast_to_float(s.getAttribute('duration')) - if duration > 0: - row['percent_complete'] = (view_offset / duration) * 100 - else: - row['percent_complete'] = 0 - - rows.append(row) - - dict = {'recordsFiltered': query['filteredCount'], - 'recordsTotal': query['totalCount'], - 'data': rows, - } - - 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 = ?' % (self.get_history_table_name()) - xml = myDB.select_single(query, args=[row_id]) - 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 - - def get_recently_watched(self, user=None, limit='10'): - myDB = db.DBConnection() - recently_watched = [] - - if not limit.isdigit(): - limit = '10' - - try: - if user: - query = 'SELECT time, user, xml FROM %s WHERE user = ? ORDER BY time DESC LIMIT ?' % \ - (self.get_history_table_name()) - xml = myDB.select(query, args=[user, limit]) - else: - query = 'SELECT time, user, xml FROM %s ORDER BY time DESC LIMIT ?' % \ - (self.get_history_table_name()) - xml = myDB.select(query, args=[limit]) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - for row in xml: - xml_data = helpers.latinToAscii(row[2]) - 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 - - for a in xml_head: - if self.get_xml_attr(a, 'type') == 'episode': - thumb = self.get_xml_attr(a, 'parentThumb') - else: - thumb = self.get_xml_attr(a, 'thumb') - - recent_output = {'type': self.get_xml_attr(a, 'type'), - 'rating_key': self.get_xml_attr(a, 'ratingKey'), - 'title': self.get_xml_attr(a, 'title'), - 'thumb': thumb, - 'index': self.get_xml_attr(a, 'index'), - 'parentIndex': self.get_xml_attr(a, 'parentIndex'), - 'year': self.get_xml_attr(a, 'year'), - 'time': row[0], - 'user': row[1] - } - recently_watched.append(recent_output) - - return recently_watched - - def get_user_watch_time_stats(self, user=None): - myDB = db.DBConnection() - - time_queries = [1, 7, 30, 0] - user_watch_time_stats = [] - - for days in time_queries: - if days > 0: - query = 'SELECT (SUM(stopped - time) - ' \ - 'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \ - 'COUNT(id) AS total_plays ' \ - 'FROM %s ' \ - 'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \ - 'AND user = ?' % (self.get_history_table_name(), days) - result = myDB.select(query, args=[user]) - else: - query = 'SELECT (SUM(stopped - time) - ' \ - 'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \ - 'COUNT(id) AS total_plays ' \ - 'FROM %s ' \ - 'WHERE user = ?' % self.get_history_table_name() - result = myDB.select(query, args=[user]) - - for item in result: - if item[0]: - total_time = item[0] - total_plays = item[1] - else: - total_time = 0 - total_plays = 0 - - row = {'query_days': days, - 'total_time': total_time, - 'total_plays': total_plays - } - - user_watch_time_stats.append(row) - - return user_watch_time_stats - - def get_user_platform_stats(self, user=None): - myDB = db.DBConnection() - - platform_stats = [] - result_id = 0 - - try: - query = 'SELECT platform, COUNT(platform) as platform_count, xml ' \ - 'FROM grouped ' \ - 'WHERE user = ? ' \ - 'GROUP BY platform ' \ - 'ORDER BY platform_count DESC' - result = myDB.select(query, args=[user]) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - for item in result: - xml_data = helpers.latinToAscii(item[2]) - - try: - xml_parse = minidom.parseString(xml_data) - except: - logger.warn("Error parsing XML for Plex stream data.") - return None - - xml_head = xml_parse.getElementsByTagName('Player') - if not xml_head: - logger.warn("Error parsing XML for Plex stream data.") - return None - - for a in xml_head: - platform_type = self.get_xml_attr(a, 'platform') - - row = {'platform_name': item[0], - 'platform_type': platform_type, - 'total_plays': item[1], - 'result_id': result_id - } - platform_stats.append(row) - result_id += 1 - - return platform_stats - - def get_user_gravatar_image(self, user=None): - myDB = db.DBConnection() - user_info = None - - try: - query = 'SELECT xml ' \ - 'FROM %s ' \ - 'WHERE user = ? ' \ - 'ORDER BY id DESC LIMIT 1' % self.get_history_table_name() - result = myDB.select_single(query, args=[user]) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - xml_data = helpers.latinToAscii(result) - - try: - xml_parse = minidom.parseString(xml_data) - except: - logger.warn("Error parsing XML for Plexwatch Database.") - return None - - xml_head = xml_parse.getElementsByTagName('User') - if not xml_head: - logger.warn("Error parsing XML for Plexwatch Database.") - return None - - for a in xml_head: - user_id = self.get_xml_attr(a, 'id') - user_thumb = self.get_xml_attr(a, 'thumb') - - user_info = {'user_id': user_id, - 'user_thumb': user_thumb} - - 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", "popular_tv", "top_users", "top_platforms"] - home_stats = [] - - for stat in stats_queries: - if 'top_tv' in stat: - top_tv = [] - try: - 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_history_table_name(), time_range) - result = myDB.select(query) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - 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 = {'title': item[0], - 'total_plays': item[1], - 'users_watched': '', - 'rating_key': item[2], - 'last_play': item[3], - 'grandparent_thumb': grandparent_thumb, - 'thumb': '', - 'user': '', - 'friendly_name': '', - 'platform_type': '', - 'platform': '' - } - top_tv.append(row) - - home_stats.append({'stat_id': stat, - 'rows': top_tv}) - - elif 'popular_tv' in stat: - popular_tv = [] - try: - query = 'SELECT orig_title, COUNT(DISTINCT user) as users_watched, grandparentRatingKey, ' \ - 'MAX(time) as last_watch, xml, COUNT(id) as total_plays ' \ - 'FROM %s ' \ - 'WHERE datetime(stopped, "unixepoch", "localtime") ' \ - '>= datetime("now", "-%s days", "localtime") ' \ - 'AND episode != "" ' \ - 'GROUP BY orig_title ' \ - 'ORDER BY users_watched DESC, total_plays DESC ' \ - 'LIMIT 10' % (self.get_history_table_name(), time_range) - result = myDB.select(query) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - 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 = {'title': item[0], - 'users_watched': item[1], - 'rating_key': item[2], - 'last_play': item[3], - 'total_plays': item[5], - 'grandparent_thumb': grandparent_thumb, - 'thumb': '', - 'user': '', - 'friendly_name': '', - 'platform_type': '', - 'platform': '' - } - popular_tv.append(row) - - home_stats.append({'stat_id': stat, - 'rows': popular_tv}) - - elif 'top_users' in stat: - top_users = [] - try: - s = self.get_history_table_name() - query = 'SELECT user, ' \ - '(case when friendly_name is null then user else friendly_name end) as friendly_name,' \ - 'COUNT(' + s + '.id) as total_plays, MAX(time) as last_watch, thumb ' \ - 'FROM ' + s + ' ' \ - 'LEFT OUTER JOIN plexpy_users ON ' + s + '.user = plexpy_users.username ' \ - 'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \ - 'datetime("now", "-' + time_range + ' days", "localtime") '\ - 'GROUP BY ' + s + '.user ' \ - 'ORDER BY total_plays DESC LIMIT 10' - result = myDB.select(query) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - for item in result: - if not item['thumb'] or item['thumb'] == '': - user_thumb = common.DEFAULT_USER_THUMB - else: - user_thumb = item[4] - - thumb = self.get_user_gravatar_image(item[0]) - row = {'user': item[0], - 'friendly_name': item[1], - 'total_plays': item[2], - 'last_play': item[3], - 'thumb': user_thumb, - 'grandparent_thumb': '', - 'users_watched': '', - 'rating_key': '', - 'title': '', - 'platform_type': '', - 'platform': '' - } - top_users.append(row) - - home_stats.append({'stat_id': stat, - 'rows': top_users}) - - elif 'top_platforms' in stat: - top_platform = [] - - try: - 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_history_table_name(), time_range) - result = myDB.select(query) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - 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, - 'title': '', - 'thumb': '', - 'grandparent_thumb': '', - 'users_watched': '', - 'rating_key': '', - 'user': '', - 'friendly_name': '' - } - 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 - - def get_total_plays_per_day(self, time_range='30'): - myDB = db.DBConnection() - - if not time_range.isdigit(): - time_range = '30' - - try: - query = 'SELECT date(time, "unixepoch", "localtime") as date_played, ' \ - 'SUM(case when episode = "" then 0 else 1 end) as tv_count, ' \ - 'SUM(case when episode = "" then 1 else 0 end) as movie_count ' \ - 'FROM %s ' \ - 'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \ - 'GROUP BY date_played ' \ - 'ORDER BY time ASC' % (self.get_history_table_name(), time_range) - - result = myDB.select(query) - except: - logger.warn("Unable to open PlexWatch database.") - return None - - # create our date range as some days may not have any data - # but we still want to display them - base = datetime.date.today() - date_list = [base - datetime.timedelta(days=x) for x in range(0, int(time_range))] - - categories = [] - series_1 = [] - series_2 = [] - - for date_item in sorted(date_list): - date_string = date_item.strftime('%Y-%m-%d') - categories.append(date_string) - series_1_value = 0 - series_2_value = 0 - for item in result: - if date_string == item[0]: - series_1_value = item[1] - series_2_value = item[2] - break - else: - series_1_value = 0 - series_2_value = 0 - - series_1.append(series_1_value) - series_2.append(series_2_value) - - series_1_output = {'name': 'TV', - 'data': series_1} - series_2_output = {'name': 'Movies', - 'data': series_2} - - output = {'categories': categories, - 'series': [series_1_output, series_2_output]} - return output - - def get_total_plays_per_dayofweek(self, time_range='30'): - myDB = db.DBConnection() - - if not time_range.isdigit(): - time_range = '30' - - query = 'SELECT strftime("%w", datetime(time, "unixepoch", "localtime")) as daynumber, ' \ - 'case cast (strftime("%w", datetime(time, "unixepoch", "localtime")) as integer) ' \ - 'when 0 then "Sunday" ' \ - 'when 1 then "Monday" ' \ - 'when 2 then "Tuesday" ' \ - 'when 3 then "Wednesday" ' \ - 'when 4 then "Thursday" ' \ - 'when 5 then "Friday" ' \ - 'else "Saturday" end as dayofweek, ' \ - 'COUNT(id) as total_plays ' \ - 'from ' + self.get_history_table_name() + ' ' + \ - 'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \ - 'datetime("now", "-' + time_range + ' days", "localtime") ' \ - 'GROUP BY dayofweek ' \ - 'ORDER BY daynumber' - - result = myDB.select(query) - - days_list = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', - 'Thursday', 'Friday', 'Saturday'] - - categories = [] - series_1 = [] - - for day_item in days_list: - categories.append(day_item) - series_1_value = 0 - for item in result: - if day_item == item[1]: - series_1_value = item[2] - break - else: - series_1_value = 0 - - series_1.append(series_1_value) - - series_1_output = {'name': 'Total plays', - 'data': series_1} - - output = {'categories': categories, - 'series': [series_1_output]} - return output - - def get_total_plays_per_hourofday(self, time_range='30'): - myDB = db.DBConnection() - - if not time_range.isdigit(): - time_range = '30' - - query = 'select strftime("%H", datetime(time, "unixepoch", "localtime")) as hourofday, ' \ - 'COUNT(id) ' \ - 'FROM ' + self.get_history_table_name() + ' ' + \ - 'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \ - 'datetime("now", "-' + time_range + ' days", "localtime") ' \ - 'GROUP BY hourofday ' \ - 'ORDER BY hourofday' - - result = myDB.select(query) - - hours_list = ['00','01','02','03','04','05', - '06','07','08','09','10','11', - '12','13','14','15','16','17', - '18','19','20','21','22','23'] - - categories = [] - series_1 = [] - - for hour_item in hours_list: - categories.append(hour_item) - series_1_value = 0 - for item in result: - if hour_item == item[0]: - series_1_value = item[1] - break - else: - series_1_value = 0 - - series_1.append(series_1_value) - - series_1_output = {'name': 'Total plays', - 'data': series_1} - - output = {'categories': categories, - 'series': [series_1_output]} - return output - - def set_user_friendly_name(self, user=None, friendly_name=None): - if user: - if friendly_name.strip() == '': - friendly_name = None - - myDB = db.DBConnection() - - control_value_dict = {"username": user} - new_value_dict = {"friendly_name": friendly_name} - - myDB.upsert('plexpy_users', new_value_dict, control_value_dict) - - def get_user_friendly_name(self, user=None): - if user: - try: - myDB = db.DBConnection() - query = 'select friendly_name FROM plexpy_users WHERE username = ?' - result = myDB.select_single(query, args=[user]) - if result: - return result - else: - return user - except: - return user - - return None - - def get_user_id(self, user=None): - if user: - try: - myDB = db.DBConnection() - query = 'select user_id FROM plexpy_users WHERE username = ?' - result = myDB.select_single(query, args=[user]) - if result: - return result - else: - return None - except: - return None - - return None - - def get_user_details(self, user=None, user_id=None): - try: - myDB = db.DBConnection() - t = self.get_history_table_name() - - if user: - query = 'SELECT user_id, username, friendly_name, email, ' \ - 'thumb, is_home_user, is_allow_sync, is_restricted ' \ - 'FROM plexpy_users ' \ - 'WHERE username = ? ' \ - 'UNION ALL ' \ - 'SELECT null, user, null, null, null, null, null, null ' \ - 'FROM %s ' \ - 'WHERE user = ? ' \ - 'GROUP BY user ' \ - 'LIMIT 1' % t - result = myDB.select(query, args=[user, user]) - elif user_id: - query = 'select user_id, username, friendly_name, email, thumb, ' \ - 'is_home_user, is_allow_sync, is_restricted FROM plexpy_users WHERE user_id = ? LIMIT 1' - result = myDB.select(query, args=[user_id]) - if result: - for item in result: - if not item['friendly_name']: - friendly_name = item['username'] - else: - friendly_name = item['friendly_name'] - if not item['thumb'] or item['thumb'] == '': - user_thumb = common.DEFAULT_USER_THUMB - else: - user_thumb = item['thumb'] - - user_details = {"user_id": item['user_id'], - "username": item['username'], - "friendly_name": friendly_name, - "email": item['email'], - "thumb": user_thumb, - "is_home_user": item['is_home_user'], - "is_allow_sync": item['is_allow_sync'], - "is_restricted": item['is_restricted'] - } - return user_details - else: - return None - except: - return None - - return None - - # 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 = dict((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 - - -def check_db_tables(): - try: - myDB = db.DBConnection() - query = 'CREATE TABLE IF NOT EXISTS plexpy_users (id INTEGER PRIMARY KEY AUTOINCREMENT, ' \ - 'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL UNIQUE, ' \ - 'friendly_name TEXT, thumb TEXT, email TEXT, is_home_user INTEGER DEFAULT NULL, ' \ - 'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL)' - result = myDB.action(query) - except: - logger.debug(u"Unable to create users table.") diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 229cb6c7..9da5b041 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.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, plexwatch, http_handler +from plexpy import logger, helpers, datafactory, http_handler import plexpy @@ -495,7 +495,7 @@ class PmsConnect(object): """ def get_session_each(self, stream_type='', session=None): session_output = None - plex_watch = plexwatch.PlexWatch() + data_factory = datafactory.DataFactory() if stream_type == 'track': media_info = session.getElementsByTagName('Media')[0] @@ -521,7 +521,7 @@ class PmsConnect(object): transcode_container = '' transcode_protocol = '' - user_details = plex_watch.get_user_details( + user_details = data_factory.get_user_details( user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title')) if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Track'): @@ -629,7 +629,7 @@ class PmsConnect(object): else: use_indexes = 0 - user_details = plex_watch.get_user_details( + user_details = data_factory.get_user_details( user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title')) if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Video'): diff --git a/plexpy/webserve.py b/plexpy/webserve.py index e1b83b3b..b65a1e0e 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, notifiers, plextv, pmsconnect, plexwatch, db, common, log_reader, datafactory +from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory from plexpy.helpers import checked, radio from mako.lookup import TemplateLookup @@ -80,13 +80,6 @@ class WebInterface(object): cherrypy.response.headers['Content-type'] = 'application/json' return json.dumps(formats) - @cherrypy.expose - def home_stats_old(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", data=stats_data) - @cherrypy.expose def home_stats(self, time_range='30', **kwargs): data_factory = datafactory.DataFactory() @@ -396,12 +389,12 @@ class WebInterface(object): # Write the config plexpy.CONFIG.write() - # Check if we have our users table - plexwatch.check_db_tables() - # Reconfigure scheduler plexpy.initialize_scheduler() + # Refresh users table. Probably shouldn't do this on every config save, will improve this later. + threading.Thread(target=plextv.refresh_users).start() + raise cherrypy.HTTPRedirect("config") @cherrypy.expose @@ -432,9 +425,9 @@ class WebInterface(object): @cherrypy.expose def clear_all_history(self, **kwargs): - from plexpy import monitor + from plexpy import database - threading.Thread(target=monitor.clear_history_tables).start() + threading.Thread(target=database.clear_history_tables).start() raise cherrypy.HTTPRedirect("config") @cherrypy.expose