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