diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f1af865..503e04a4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## v1.1.10 (2015-09-20) + +* Added dedicated settings section for home stats configuration with ability to show/hide selected stats and sections. +* Added support for Twitter notifications. +* Only show music in graphs if music logging is enabled. +* The monitoring ignore interval now excludes paused time. +* Fix display bug on activity panel which incorrectly reported transcoding sometimes. +* Fix bug with Email notification TLS checkbox when it would be disabled by changing any other settings afterwards. +* Fix issue on some Python releases where the webbrowser library isn't included. + + ## v1.1.9 (2015-09-14) * Another JonnyWong release. I'm going to stop thanking you now ;) diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index ff44dbea..649f36ac 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -33,6 +33,33 @@ select.input-sm { color: #999; outline: none; } +select[multiple] { + height: 125px; + margin: 5px 0 5px 0; + color: #fff; + border: 0px solid #444; + background: #555; + padding: 2px 2px; + background-color: #555; + border-radius: 3px; + transition: background-color .3s; +} +select[multiple]:focus { + outline: 0; + outline: thin dotted \9; + color: #555; + background-color: #fff; + transition: background-color .3s; +} +select[multiple]:focus::-webkit-scrollbar-thumb { + background-color: rgba(0,0,0,.15); +} +select[multiple] option { + padding: 6px 10px; + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; +} img { -webkit-box-sizing: content-box; -moz-box-sizing: content-box; diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index 04632966..00a96048 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -119,6 +119,8 @@ DOCUMENTATION :: END % if a['type'] == 'track': % if a['audio_decision'] == 'direct play': Stream  Direct Play + % elif a['audio_decision'] == 'copy': + Stream  Direct Stream % else: Stream  Transcoding (Speed: ${a['transcode_speed']}) @@ -136,8 +138,10 @@ DOCUMENTATION :: END Audio  Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch) % endif % elif a['type'] == 'episode' or a['type'] == 'movie' or a['type'] == 'clip': - % if a['video_decision'] == 'direct play': + % if a['video_decision'] == 'direct play' and a['audio_decision'] == 'direct play': Stream  Direct Play + % elif a['video_decision'] == 'copy' and a['audio_decision'] == 'copy': + Stream  Direct Stream % else: Stream  Transcoding (Speed: ${a['transcode_speed']}) @@ -165,6 +169,8 @@ DOCUMENTATION :: END % elif a['type'] == 'photo': % if a['video_decision'] == 'direct play': Stream  Direct Play + % elif a['video_decision'] == 'copy': + Stream  Direct Stream % else: Stream   Transcoding diff --git a/data/interfaces/default/graphs.html b/data/interfaces/default/graphs.html index af6ed442..8f114b67 100644 --- a/data/interfaces/default/graphs.html +++ b/data/interfaces/default/graphs.html @@ -294,6 +294,8 @@ $('a[data-toggle=tab][href=' + current_tab + ']').trigger('click'); } + var music_visible = (${config['music_logging_enable']} == 1 ? true : false); + function loadGraphsTab1(time_range, yaxis) { setGraphFormat(yaxis); @@ -319,6 +321,7 @@ hc_plays_by_day_options.yAxis.min = 0; hc_plays_by_day_options.xAxis.categories = dateArray; hc_plays_by_day_options.series = data.series; + hc_plays_by_day_options.series[2].visible = music_visible; var hc_plays_by_day = new Highcharts.Chart(hc_plays_by_day_options); } }); @@ -331,6 +334,7 @@ success: function(data) { hc_plays_by_dayofweek_options.xAxis.categories = data.categories; hc_plays_by_dayofweek_options.series = data.series; + hc_plays_by_dayofweek_options.series[2].visible = music_visible; var hc_plays_by_dayofweek = new Highcharts.Chart(hc_plays_by_dayofweek_options); } }); @@ -343,6 +347,7 @@ success: function(data) { hc_plays_by_hourofday_options.xAxis.categories = data.categories; hc_plays_by_hourofday_options.series = data.series; + hc_plays_by_hourofday_options.series[2].visible = music_visible; var hc_plays_by_hourofday = new Highcharts.Chart(hc_plays_by_hourofday_options); } }); @@ -355,6 +360,7 @@ success: function(data) { hc_plays_by_platform_options.xAxis.categories = data.categories; hc_plays_by_platform_options.series = data.series; + hc_plays_by_platform_options.series[2].visible = music_visible; var hc_plays_by_platform = new Highcharts.Chart(hc_plays_by_platform_options); } }); @@ -367,6 +373,7 @@ success: function(data) { hc_plays_by_user_options.xAxis.categories = data.categories; hc_plays_by_user_options.series = data.series; + hc_plays_by_user_options.series[2].visible = music_visible; var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options); } }); @@ -462,6 +469,7 @@ hc_plays_by_month_options.yAxis.min = 0; hc_plays_by_month_options.xAxis.categories = data.categories; hc_plays_by_month_options.series = data.series; + hc_plays_by_month_options.series[2].visible = music_visible; var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options); } }); diff --git a/data/interfaces/default/home_stats.html b/data/interfaces/default/home_stats.html index 01fb0cdd..36c17d7f 100644 --- a/data/interfaces/default/home_stats.html +++ b/data/interfaces/default/home_stats.html @@ -17,19 +17,19 @@ data[array_index]['rows'] :: Usable parameters row_id Return the db row id for a metadata item if one exists -== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_movies' or 'popular_movies' or 'last_watched' == +== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_movies' or 'popular_movies' or 'top_music' or 'popular_music' or 'last_watched' == thumb Return the thumb for the media item. -== Only if 'stat_id' is 'top_tv' or 'popular_tv' == +== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_music' or 'popular_music' == grandparent_thumb Returns location of the item's thumbnail. Use with pms_image_proxy. rating_key Returns the unique identifier for the media item. title Returns the title for the associated stat. -== Only if 'stat_id' is 'top_tv' or 'top_movies' or 'top_user' or 'top_platform' == +== Only if 'stat_id' is 'top_tv' or 'top_movies' or 'top_music' or 'top_user' or 'top_platform' == total_plays Returns the count for the associated stat. total_duration Returns the total duration for the associated stat. -== Only of 'stat_id' is 'popular_tv' or 'popular_movies' == +== Only of 'stat_id' is 'popular_tv' or 'popular_movies' or 'popular_music' == users_watched Returns the count for the associated stat. == Only if 'stat_id' is 'top_user' or 'last_watched' == @@ -372,6 +372,158 @@ DOCUMENTATION :: END % endif + % elif top_stat['stat_id'] == 'top_music' and top_stat['rows']: +
+
  • +
    +
    +

    Most Listened to Artist

    +
    +
    +

    + + ${top_stat['rows'][0]['title']} + +

    + % if top_stat['stat_type'] == 'total_plays': +

    ${top_stat['rows'][0]['total_plays']}

    +

    plays

    + % else: + ${top_stat['rows'][0]['total_duration'] | hd} + % endif +
    +
    + + % if top_stat['rows'][0]['grandparent_thumb']: +
    +
    +
    + % else: +
    +
    +
    + % endif +
    + %if len(top_stat['rows']) > 1: +
    + + % endif +
  • +
    + % elif top_stat['stat_id'] == 'popular_music' and top_stat['rows']: +
    +
  • +
    +
    +

    Most Popular Artist

    +
    +
    +

    + + ${top_stat['rows'][0]['title']} + +

    +

    ${top_stat['rows'][0]['users_watched']}

    +

    users

    +
    +
    + + % if top_stat['rows'][0]['grandparent_thumb'] != '': +
    +
    +
    + % else: +
    +
    +
    + % endif +
    + %if len(top_stat['rows']) > 1: +
    + + % endif +
  • +
    % elif top_stat['stat_id'] == 'top_users' and top_stat['rows']:
  • diff --git a/data/interfaces/default/index.html b/data/interfaces/default/index.html index b73bdbe0..5f8caeba 100644 --- a/data/interfaces/default/index.html +++ b/data/interfaces/default/index.html @@ -16,6 +16,7 @@
  • + % if config['home_stats_cards'] > 'watch_statistics':
    @@ -27,6 +28,8 @@
    + % endif + % if config['home_library_cards'] > 'library_statistics':
    @@ -38,6 +41,7 @@
    + % endif
    @@ -82,12 +86,12 @@ currentActivity(); setInterval(currentActivity, 15000); - function getHomeStats(days, stat_type, stat_count) { + function getHomeStats(days) { $.ajax({ url: 'home_stats', cache: false, async: true, - data: {time_range: days, stat_type: stat_type, stat_count: stat_count}, + data: { }, complete: function(xhr, status) { $("#home-stats").html(xhr.responseText); } @@ -165,7 +169,7 @@ } }); - getHomeStats(${config['home_stats_length']}, ${config['home_stats_type']}, ${config['home_stats_count']}); + getHomeStats(); getLibraryStatsHeader(); getLibraryStats(); diff --git a/data/interfaces/default/library_stats.html b/data/interfaces/default/library_stats.html index 34ef2a91..12e7135c 100644 --- a/data/interfaces/default/library_stats.html +++ b/data/interfaces/default/library_stats.html @@ -73,6 +73,6 @@ DOCUMENTATION :: END % endfor % else: -
    Unable to retrieve data from database. Please check your settings. +
    Unable to retrieve data from server. Please check your settings.

    % endif \ No newline at end of file diff --git a/data/interfaces/default/notification_config.html b/data/interfaces/default/notification_config.html index 58f6436e..8e685d89 100644 --- a/data/interfaces/default/notification_config.html +++ b/data/interfaces/default/notification_config.html @@ -1,3 +1,6 @@ +<%! +from plexpy import helpers +%> % if data: +
    -

    Homepage Statistics

    +

    Watch Statistics

    +
    + +
    +
    + +
    +
    +

    Select the cards to show in the watch statistics on the home page. Select none to disable.

    +
    @@ -95,7 +119,7 @@ available_notification_agents = notifiers.available_notification_agents()
    -

    Specify the number of days for the statistics on the home page. Default is 30 days.

    +

    Specify the number of days for the watch statistics on the home page. Default is 30 days.

    @@ -105,7 +129,7 @@ available_notification_agents = notifiers.available_notification_agents()
    -

    Specify the number of items to show in the top lists for the statistics on the home page. Max is 10 items, default is 5 items, 0 to disable.

    +

    Specify the number of items to show in the top lists for the watch statistics on the home page. Max is 10 items, default is 5 items, 0 to disable.

    Use play duration instead of play count to generate statistics.

    + +
    +

    Library Statistics

    +
    + +
    + +
    +
    + +
    +
    +

    Select the cards to show in the library statistics on the home page. Select none to disable.

    +

    +
    -
    +

    Web Interface

    @@ -164,7 +205,7 @@ available_notification_agents = notifiers.available_notification_agents()

    -
    +

    Authentication

    @@ -216,7 +257,7 @@ available_notification_agents = notifiers.available_notification_agents()

    -
    +

    Plex Media Server

    @@ -272,7 +313,7 @@ available_notification_agents = notifiers.available_notification_agents()
    -
    +

    Plex.tv Authentication

    @@ -315,7 +356,7 @@ available_notification_agents = notifiers.available_notification_agents()

    -
    +

    Extra Settings

    @@ -334,7 +375,7 @@ available_notification_agents = notifiers.available_notification_agents()

    -
    +

    Monitoring Settings

    @@ -367,7 +408,7 @@ available_notification_agents = notifiers.available_notification_agents()
    -

    The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.

    +

    The interval (in seconds) an item must be in a playing state before logging it. 0 to disable.

    -
    +

    Global Notification Toggles

    @@ -566,7 +607,7 @@ available_notification_agents = notifiers.available_notification_agents()

    -
    +

    Notification Agents

    @@ -1178,6 +1219,47 @@ $(document).ready(function() { } var accordion = new Accordion($('#accordion'), false); + + var cards = "${config['home_stats_cards']}".split(/[\s,]+/); + cards.forEach(function (item) { + $('#card-'+item).prop('selected', !$(this).prop('selected')); + }); + $('#home_stats_cards').on('mousedown', function(e) { + e.preventDefault(); + var scroll = this.scrollTop; + e.target.selected = !e.target.selected; + this.scrollTop = scroll; + }).on('mousemove', function(e) { + e.preventDefault() + }); + + $.ajax({ + url: 'get_server_children', + data: { }, + async: true, + complete: function (xhr, status) { + server_children_info = $.parseJSON(xhr.responseText); + libraries_list = server_children_info.libraries_list; + for (var i in libraries_list) { + title = libraries_list[i].title; + key = libraries_list[i].key; + $('#home_library_cards').append('') + } + var cards = "${config['home_library_cards']}".split(/[\s,]+/); + cards.forEach(function (item) { + $('#card-'+item).prop('selected', !$(this).prop('selected')); + }); + } + }); + $('#home_library_cards').on('mousedown', function(e) { + e.preventDefault(); + var scroll = this.scrollTop; + e.target.selected = !e.target.selected; + this.scrollTop = scroll; + }).on('mousemove', function(e) { + e.preventDefault() + }); + }); diff --git a/plexpy/__init__.py b/plexpy/__init__.py index ad02333c..fc9639c4 100644 --- a/plexpy/__init__.py +++ b/plexpy/__init__.py @@ -17,11 +17,16 @@ import os import sys import subprocess import threading -import webbrowser import sqlite3 import cherrypy import datetime import uuid +# Some cut down versions of Python may not include this module and it's not critical for us +try: + import webbrowser + no_browser = False +except ImportError: + no_browser = True from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger @@ -228,18 +233,19 @@ def daemonize(): def launch_browser(host, port, root): - if host == '0.0.0.0': - host = 'localhost' + if not no_browser: + if host == '0.0.0.0': + host = 'localhost' - if CONFIG.ENABLE_HTTPS: - protocol = 'https' - else: - protocol = 'http' + if CONFIG.ENABLE_HTTPS: + protocol = 'https' + else: + protocol = 'http' - try: - webbrowser.open('%s://%s:%i%s' % (protocol, host, port, root)) - except Exception as e: - logger.error('Could not launch browser: %s', e) + try: + webbrowser.open('%s://%s:%i%s' % (protocol, host, port, root)) + except Exception as e: + logger.error('Could not launch browser: %s', e) def initialize_scheduler(): diff --git a/plexpy/config.py b/plexpy/config.py index 51145685..73034be2 100644 --- a/plexpy/config.py +++ b/plexpy/config.py @@ -82,9 +82,11 @@ _CONFIG_DEFINITIONS = { 'GROWL_ON_RESUME': (int, 'Growl', 0), 'GROWL_ON_BUFFER': (int, 'Growl', 0), 'GROWL_ON_WATCHED': (int, 'Growl', 0), + 'HOME_LIBRARY_CARDS': (str, 'General', 'library_statistics_first'), 'HOME_STATS_LENGTH': (int, 'General', 30), 'HOME_STATS_TYPE': (int, 'General', 0), 'HOME_STATS_COUNT': (int, 'General', 5), + 'HOME_STATS_CARDS': (str, 'General', 'watch_statistics, top_tv, popular_tv, top_movies, popular_movies, top_music, popular_music, top_users, top_platforms, last_watched'), 'HTTPS_CERT': (str, 'General', ''), 'HTTPS_KEY': (str, 'General', ''), 'HTTP_HOST': (str, 'General', '0.0.0.0'), @@ -194,8 +196,14 @@ _CONFIG_DEFINITIONS = { 'TV_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0), 'TWITTER_ENABLED': (int, 'Twitter', 0), 'TWITTER_PASSWORD': (str, 'Twitter', ''), - 'TWITTER_PREFIX': (str, 'Twitter', 'Headphones'), + 'TWITTER_PREFIX': (str, 'Twitter', 'PlexPy'), 'TWITTER_USERNAME': (str, 'Twitter', ''), + 'TWITTER_ON_PLAY': (int, 'Twitter', 0), + 'TWITTER_ON_STOP': (int, 'Twitter', 0), + 'TWITTER_ON_PAUSE': (int, 'Twitter', 0), + 'TWITTER_ON_RESUME': (int, 'Twitter', 0), + 'TWITTER_ON_BUFFER': (int, 'Twitter', 0), + 'TWITTER_ON_WATCHED': (int, 'Twitter', 0), 'UPDATE_DB_INTERVAL': (int, 'General', 24), 'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1), 'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1), diff --git a/plexpy/datafactory.py b/plexpy/datafactory.py index 4a988480..40e57092 100644 --- a/plexpy/datafactory.py +++ b/plexpy/datafactory.py @@ -131,22 +131,14 @@ class DataFactory(object): return dict - def get_home_stats(self, time_range='30', stat_type='0', stat_count='5'): + def get_home_stats(self, time_range='30', stats_type=0, stats_count='5', stats_cards='', notify_watched_percent='85'): monitor_db = database.MonitorDatabase() - if not time_range.isdigit(): - time_range = '30' + sort_type = 'total_plays' if stats_type == 0 else 'total_duration' - sort_type = 'total_plays' if stat_type == '0' else 'total_duration' - - if not time_range.isdigit(): - stat_count = '5' - - # This actually determines the output order in the home page - stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms", "last_watched"] home_stats = [] - for stat in stats_queries: + for stat in stats_cards: if 'top_tv' in stat: top_tv = [] try: @@ -166,7 +158,7 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND session_history_metadata.media_type = "episode" ' \ 'GROUP BY session_history_metadata.grandparent_title ' \ - 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -202,6 +194,10 @@ class DataFactory(object): 'session_history_metadata.grandparent_rating_key, ' \ 'MAX(session_history.started) as last_watch, ' \ 'COUNT(session_history.id) as total_plays, ' \ + 'SUM(case when session_history.stopped > 0 ' \ + 'then (session_history.stopped - session_history.started) ' \ + ' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \ + 'else 0 end) as total_duration, ' \ 'session_history_metadata.grandparent_thumb ' \ 'FROM session_history_metadata ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ @@ -209,8 +205,8 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND session_history_metadata.media_type = "episode" ' \ 'GROUP BY session_history_metadata.grandparent_title ' \ - 'ORDER BY users_watched DESC, total_plays DESC ' \ - 'LIMIT %s' % (time_range, stat_count) + 'ORDER BY users_watched DESC, %s DESC ' \ + 'LIMIT %s' % (time_range, sort_type, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -222,7 +218,7 @@ class DataFactory(object): 'rating_key': item[3], 'last_play': item[4], 'total_plays': item[5], - 'grandparent_thumb': item[6], + 'grandparent_thumb': item[7], 'thumb': '', 'user': '', 'friendly_name': '', @@ -254,7 +250,7 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND session_history_metadata.media_type = "movie" ' \ 'GROUP BY session_history_metadata.full_title ' \ - 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -290,6 +286,10 @@ class DataFactory(object): 'session_history_metadata.rating_key, ' \ 'MAX(session_history.started) as last_watch, ' \ 'COUNT(session_history.id) as total_plays, ' \ + 'SUM(case when session_history.stopped > 0 ' \ + 'then (session_history.stopped - session_history.started) ' \ + ' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \ + 'else 0 end) as total_duration, ' \ 'session_history_metadata.thumb ' \ 'FROM session_history_metadata ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ @@ -297,8 +297,8 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND session_history_metadata.media_type = "movie" ' \ 'GROUP BY session_history_metadata.full_title ' \ - 'ORDER BY users_watched DESC, total_plays DESC ' \ - 'LIMIT %s' % (time_range, stat_count) + 'ORDER BY users_watched DESC, %s DESC ' \ + 'LIMIT %s' % (time_range, sort_type, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -311,7 +311,7 @@ class DataFactory(object): 'last_play': item[4], 'total_plays': item[5], 'grandparent_thumb': '', - 'thumb': item[6], + 'thumb': item[7], 'user': '', 'friendly_name': '', 'platform_type': '', @@ -323,6 +323,98 @@ class DataFactory(object): home_stats.append({'stat_id': stat, 'rows': popular_movies}) + elif 'top_music' in stat: + top_music = [] + try: + query = 'SELECT session_history_metadata.id, ' \ + 'session_history_metadata.grandparent_title, ' \ + 'COUNT(session_history_metadata.grandparent_title) as total_plays, ' \ + 'SUM(case when session_history.stopped > 0 ' \ + 'then (session_history.stopped - session_history.started) ' \ + ' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \ + 'else 0 end) as total_duration, ' \ + 'session_history_metadata.grandparent_rating_key, ' \ + 'MAX(session_history.started) as last_watch,' \ + 'session_history_metadata.grandparent_thumb ' \ + 'FROM session_history_metadata ' \ + 'JOIN session_history on session_history_metadata.id = session_history.id ' \ + 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ + '>= datetime("now", "-%s days", "localtime") ' \ + 'AND session_history_metadata.media_type = "track" ' \ + 'GROUP BY session_history_metadata.grandparent_title ' \ + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) + result = monitor_db.select(query) + except: + logger.warn("Unable to execute database query.") + return None + + for item in result: + row = {'title': item[1], + 'total_plays': item[2], + 'total_duration': item[3], + 'users_watched': '', + 'rating_key': item[4], + 'last_play': item[5], + 'grandparent_thumb': item[6], + 'thumb': '', + 'user': '', + 'friendly_name': '', + 'platform_type': '', + 'platform': '', + 'row_id': item[0] + } + top_music.append(row) + + home_stats.append({'stat_id': stat, + 'stat_type': sort_type, + 'rows': top_music}) + + elif 'popular_music' in stat: + popular_music = [] + try: + query = 'SELECT session_history_metadata.id, ' \ + 'session_history_metadata.grandparent_title, ' \ + 'COUNT(DISTINCT session_history.user_id) as users_watched, ' \ + 'session_history_metadata.grandparent_rating_key, ' \ + 'MAX(session_history.started) as last_watch, ' \ + 'COUNT(session_history.id) as total_plays, ' \ + 'SUM(case when session_history.stopped > 0 ' \ + 'then (session_history.stopped - session_history.started) ' \ + ' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \ + 'else 0 end) as total_duration, ' \ + 'session_history_metadata.grandparent_thumb ' \ + 'FROM session_history_metadata ' \ + 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ + 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ + '>= datetime("now", "-%s days", "localtime") ' \ + 'AND session_history_metadata.media_type = "track" ' \ + 'GROUP BY session_history_metadata.grandparent_title ' \ + 'ORDER BY users_watched DESC, %s DESC ' \ + 'LIMIT %s' % (time_range, sort_type, stats_count) + result = monitor_db.select(query) + except: + logger.warn("Unable to execute database query.") + return None + + for item in result: + row = {'title': item[1], + 'users_watched': item[2], + 'rating_key': item[3], + 'last_play': item[4], + 'total_plays': item[5], + 'grandparent_thumb': item[7], + 'thumb': '', + 'user': '', + 'friendly_name': '', + 'platform_type': '', + 'platform': '', + 'row_id': item[0] + } + popular_music.append(row) + + home_stats.append({'stat_id': stat, + 'rows': popular_music}) + elif 'top_users' in stat: top_users = [] try: @@ -343,7 +435,7 @@ class DataFactory(object): 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'datetime("now", "-%s days", "localtime") '\ 'GROUP BY session_history.user_id ' \ - 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -391,7 +483,7 @@ class DataFactory(object): 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ '>= datetime("now", "-%s days", "localtime") ' \ 'GROUP BY session_history.platform ' \ - 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count) + 'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") @@ -432,7 +524,11 @@ class DataFactory(object): 'session_history_metadata.thumb, ' \ 'session_history_metadata.grandparent_thumb, ' \ 'MAX(session_history.started) as last_watch, ' \ - 'session_history.player as platform ' \ + 'session_history.player as platform, ' \ + '((CASE WHEN session_history.view_offset IS NULL THEN 0.1 ELSE \ + session_history.view_offset * 1.0 END) / \ + (CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE \ + session_history_metadata.duration * 1.0 END) * 100) as percent_complete ' \ 'FROM session_history_metadata ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'LEFT OUTER JOIN users ON session_history.user_id = users.user_id ' \ @@ -440,9 +536,10 @@ class DataFactory(object): '>= datetime("now", "-%s days", "localtime") ' \ 'AND (session_history_metadata.media_type = "movie" ' \ 'OR session_history_metadata.media_type = "episode") ' \ + 'AND percent_complete >= %s ' \ 'GROUP BY session_history_metadata.full_title ' \ 'ORDER BY last_watch DESC ' \ - 'LIMIT %s' % (time_range, stat_count) + 'LIMIT %s' % (time_range, notify_watched_percent, stats_count) result = monitor_db.select(query) except: logger.warn("Unable to execute database query.") diff --git a/plexpy/monitor.py b/plexpy/monitor.py index 091fa575..a35db676 100644 --- a/plexpy/monitor.py +++ b/plexpy/monitor.py @@ -241,7 +241,7 @@ class MonitorProcessing(object): if is_import: if str(session['stopped']).isdigit(): - stopped = session['stopped'] + stopped = int(session['stopped']) else: stopped = int(time.time()) else: @@ -257,21 +257,25 @@ class MonitorProcessing(object): logger.debug(u"PlexPy Monitor :: ratingKey %s not logged. Does not meet logging criteria. " u"Media type is '%s'" % (session['rating_key'], session['media_type'])) + if str(session['paused_counter']).isdigit(): + real_play_time = stopped - session['started'] - int(session['paused_counter']) + else: + real_play_time = stopped - session['started'] + if plexpy.CONFIG.LOGGING_IGNORE_INTERVAL and not is_import: if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ - (int(stopped) - session['started'] < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)): + (real_play_time < int(plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)): logging_enabled = False logger.debug(u"PlexPy Monitor :: Play duration for ratingKey %s is %s secs which is less than %s " u"seconds, so we're not logging it." % - (session['rating_key'], str(int(stopped) - session['started']), - plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)) + (session['rating_key'], str(real_play_time), plexpy.CONFIG.LOGGING_IGNORE_INTERVAL)) elif is_import and import_ignore_interval: if (session['media_type'] == 'movie' or session['media_type'] == 'episode') and \ - (int(stopped) - session['started'] < int(import_ignore_interval)): + (real_play_time < int(import_ignore_interval)): logging_enabled = False logger.debug(u"PlexPy Monitor :: Play duration for ratingKey %s is %s secs which is less than %s " u"seconds, so we're not logging it." % - (session['rating_key'], str(int(stopped) - session['started']), + (session['rating_key'], str(real_play_time), import_ignore_interval)) if not user_details['keep_history'] and not is_import: diff --git a/plexpy/notifiers.py b/plexpy/notifiers.py index 2c348b5a..e780fef7 100644 --- a/plexpy/notifiers.py +++ b/plexpy/notifiers.py @@ -49,7 +49,8 @@ AGENT_IDS = {"Growl": 0, "Pushover": 7, "OSX Notify": 8, "Boxcar2": 9, - "Email": 10} + "Email": 10, + "Twitter": 11} def available_notification_agents(): agents = [{'name': 'Growl', @@ -171,6 +172,18 @@ def available_notification_agents(): 'on_resume': plexpy.CONFIG.EMAIL_ON_RESUME, 'on_buffer': plexpy.CONFIG.EMAIL_ON_BUFFER, 'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED + }, + {'name': 'Twitter', + 'id': AGENT_IDS['Twitter'], + 'config_prefix': 'twitter', + 'has_config': True, + 'state': checked(plexpy.CONFIG.TWITTER_ENABLED), + 'on_play': plexpy.CONFIG.TWITTER_ON_PLAY, + 'on_stop': plexpy.CONFIG.TWITTER_ON_STOP, + 'on_pause': plexpy.CONFIG.TWITTER_ON_PAUSE, + 'on_resume': plexpy.CONFIG.TWITTER_ON_RESUME, + 'on_buffer': plexpy.CONFIG.TWITTER_ON_BUFFER, + 'on_watched': plexpy.CONFIG.TWITTER_ON_WATCHED } ] @@ -229,6 +242,9 @@ def get_notification_agent_config(config_id): elif config_id == 10: email = Email() return email.return_config_options() + elif config_id == 11: + tweet = TwitterNotifier() + return tweet.return_config_options() else: return [] else: @@ -271,6 +287,9 @@ def send_notification(config_id, subject, body): elif config_id == 10: email = Email() email.notify(subject=subject, message=body) + elif config_id == 11: + tweet = TwitterNotifier() + tweet.notify(subject=subject, message=body) else: logger.debug(u"PlexPy Notifier :: Unknown agent id received.") else: @@ -912,19 +931,17 @@ class TwitterNotifier(object): SIGNIN_URL = 'https://api.twitter.com/oauth/authenticate' def __init__(self): - self.consumer_key = "oYKnp2ddX5gbARjqX8ZAAg" - self.consumer_secret = "A4Xkw9i5SjHbTk7XT8zzOPqivhj9MmRDR9Qn95YA9sk" + self.consumer_key = "2LdJKXHDUwJtjYBsdwJisIOsh" + self.consumer_secret = "QWbUcZzAIiL4zbDCIhy2EdUkV8yEEav3qMdo5y3FugxCFelWrA" - def notify_snatch(self, title): - if plexpy.CONFIG.TWITTER_ONSNATCH: - self._notifyTwitter(common.notifyStrings[common.NOTIFY_SNATCH] + ': ' + title + ' at ' + helpers.now()) - - def notify_download(self, title): - if plexpy.CONFIG.TWITTER_ENABLED: - self._notifyTwitter(common.notifyStrings[common.NOTIFY_DOWNLOAD] + ': ' + title + ' at ' + helpers.now()) + def notify(self, subject, message): + if not subject or not message: + return + else: + self._send_tweet(subject + ': ' + message) def test_notify(self): - return self._notifyTwitter("This is a test notification from PlexPy at " + helpers.now(), force=True) + return self._send_tweet("This is a test notification from PlexPy at " + helpers.now()) def _get_authorization(self): @@ -958,7 +975,7 @@ class TwitterNotifier(object): logger.info('Generating and signing request for an access token using key ' + key) oauth_consumer = oauth.Consumer(key=self.consumer_key, secret=self.consumer_secret) - logger.info('oauth_consumer: ' + str(oauth_consumer)) + # logger.debug('oauth_consumer: ' + str(oauth_consumer)) oauth_client = oauth.Client(oauth_consumer, token) logger.info('oauth_client: ' + str(oauth_client)) resp, content = oauth_client.request(self.ACCESS_TOKEN_URL, method='POST', body='oauth_verifier=%s' % key) @@ -979,7 +996,6 @@ class TwitterNotifier(object): return True def _send_tweet(self, message=None): - username = self.consumer_key password = self.consumer_secret access_token_key = plexpy.CONFIG.TWITTER_USERNAME @@ -997,13 +1013,36 @@ class TwitterNotifier(object): return True - def _notifyTwitter(self, message='', force=False): - prefix = plexpy.CONFIG.TWITTER_PREFIX + def return_config_options(self): + config_option = [{'label': 'Request Authorisation', + 'value': 'Request Authorisation', + 'name': 'twitterStep1', + 'description': 'Step 1: Click Request button above. (Ensure you allow the browser pop-up).', + 'input_type': 'button' + }, + {'label': 'Authorisation Key', + 'value': '', + 'name': 'twitter_key', + 'description': 'Step 2: Input the authorisation key you received from Step 1.', + 'input_type': 'text' + }, + {'label': 'Verify Key', + 'value': 'Verify Key', + 'name': 'twitterStep2', + 'description': 'Step 3: Verify the key.', + 'input_type': 'button' + }, + {'label': 'Test Twitter', + 'value': 'Test Twitter', + 'name': 'testTwitter', + 'description': 'Test if Twitter notifications are working. See logs for troubleshooting.', + 'input_type': 'button' + }, + {'input_type': 'nosave' + } + ] - if not plexpy.CONFIG.TWITTER_ENABLED and not force: - return False - - return self._send_tweet(prefix + ": " + message) + return config_option class OSX_NOTIFY(object): @@ -1204,7 +1243,7 @@ class Email(object): 'input_type': 'password' }, {'label': 'TLS', - 'value': checked(plexpy.CONFIG.EMAIL_TLS), + 'value': plexpy.CONFIG.EMAIL_TLS, 'name': 'email_tls', 'description': 'Does the server use encryption.', 'input_type': 'checkbox' diff --git a/plexpy/pmsconnect.py b/plexpy/pmsconnect.py index 25b23253..0fa2e061 100644 --- a/plexpy/pmsconnect.py +++ b/plexpy/pmsconnect.py @@ -1208,7 +1208,7 @@ class PmsConnect(object): 'title': helpers.get_xml_attr(xml_head[0], 'title1'), 'libraries_list': libraries_list } - + return output """ @@ -1270,11 +1270,11 @@ class PmsConnect(object): return output """ - Return processed and validated server statistics. + Return processed and validated library statistics. Output: array """ - def get_library_stats(self): + def get_library_stats(self, library_cards=''): server_libraries = self.get_server_children() server_library_stats = [] @@ -1285,7 +1285,10 @@ class PmsConnect(object): for library in libraries_list: library_type = library['type'] section_key = library['key'] - library_list = self.get_library_children(library_type, section_key) + if section_key in library_cards: + library_list = self.get_library_children(library_type, section_key) + else: + continue if library_list['library_count'] != '0': library_stats = {'title': library['title'], diff --git a/plexpy/version.py b/plexpy/version.py index 6af938ca..b8610db2 100644 --- a/plexpy/version.py +++ b/plexpy/version.py @@ -1,2 +1,2 @@ PLEXPY_VERSION = "master" -PLEXPY_RELEASE_VERSION = "1.1.9" +PLEXPY_RELEASE_VERSION = "1.1.10" diff --git a/plexpy/webserve.py b/plexpy/webserve.py index 6bb6f9ac..60d9bf4f 100644 --- a/plexpy/webserve.py +++ b/plexpy/webserve.py @@ -66,9 +66,9 @@ class WebInterface(object): def home(self): config = { "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, - "home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE, - "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, - "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER, + "home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS, + "home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS, + "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER } return serve_template(templatename="index.html", title="Home", config=config) @@ -121,16 +121,41 @@ class WebInterface(object): return json.dumps(formats) @cherrypy.expose - def home_stats(self, time_range='30', stat_type='0', stat_count='5', **kwargs): + def home_stats(self, **kwargs): data_factory = datafactory.DataFactory() - stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type, stat_count=stat_count) + + time_range = plexpy.CONFIG.HOME_STATS_LENGTH + stats_type = plexpy.CONFIG.HOME_STATS_TYPE + stats_count = plexpy.CONFIG.HOME_STATS_COUNT + stats_cards = plexpy.CONFIG.HOME_STATS_CARDS.split(', ') + notify_watched_percent = plexpy.CONFIG.NOTIFY_WATCHED_PERCENT + + stats_data = data_factory.get_home_stats(time_range=time_range, + stats_type=stats_type, + stats_count=stats_count, + stats_cards=stats_cards, + notify_watched_percent=notify_watched_percent) return serve_template(templatename="home_stats.html", title="Stats", data=stats_data) @cherrypy.expose def library_stats(self, **kwargs): pms_connect = pmsconnect.PmsConnect() - stats_data = pms_connect.get_library_stats() + + library_cards = plexpy.CONFIG.HOME_LIBRARY_CARDS.split(', ') + + if library_cards == ['library_statistics_first']: + library_cards = ['library_statistics'] + server_children = pms_connect.get_server_children() + server_libraries = server_children['libraries_list'] + + for library in server_libraries: + library_cards.append(library['key']) + + plexpy.CONFIG.HOME_LIBRARY_CARDS = ', '.join(library_cards) + plexpy.CONFIG.write() + + stats_data = pms_connect.get_library_stats(library_cards=library_cards) return serve_template(templatename="library_stats.html", title="Library Stats", data=stats_data) @@ -144,7 +169,12 @@ class WebInterface(object): @cherrypy.expose def graphs(self): - return serve_template(templatename="graphs.html", title="Graphs") + + config = { + "music_logging_enable": plexpy.CONFIG.MUSIC_LOGGING_ENABLE + } + + return serve_template(templatename="graphs.html", title="Graphs", config=config) @cherrypy.expose def sync(self): @@ -373,46 +403,7 @@ class WebInterface(object): "cache_dir": plexpy.CONFIG.CACHE_DIR, "check_github": checked(plexpy.CONFIG.CHECK_GITHUB), "interface_list": interface_list, - "growl_enabled": checked(plexpy.CONFIG.GROWL_ENABLED), - "growl_host": plexpy.CONFIG.GROWL_HOST, - "growl_password": plexpy.CONFIG.GROWL_PASSWORD, - "prowl_enabled": checked(plexpy.CONFIG.PROWL_ENABLED), - "prowl_keys": plexpy.CONFIG.PROWL_KEYS, - "prowl_priority": plexpy.CONFIG.PROWL_PRIORITY, - "xbmc_enabled": checked(plexpy.CONFIG.XBMC_ENABLED), - "xbmc_host": plexpy.CONFIG.XBMC_HOST, - "xbmc_username": plexpy.CONFIG.XBMC_USERNAME, - "xbmc_password": plexpy.CONFIG.XBMC_PASSWORD, - "plex_enabled": checked(plexpy.CONFIG.PLEX_ENABLED), - "plex_client_host": plexpy.CONFIG.PLEX_CLIENT_HOST, - "plex_username": plexpy.CONFIG.PLEX_USERNAME, - "plex_password": plexpy.CONFIG.PLEX_PASSWORD, - "nma_enabled": checked(plexpy.CONFIG.NMA_ENABLED), - "nma_apikey": plexpy.CONFIG.NMA_APIKEY, - "nma_priority": int(plexpy.CONFIG.NMA_PRIORITY), - "pushalot_enabled": checked(plexpy.CONFIG.PUSHALOT_ENABLED), - "pushalot_apikey": plexpy.CONFIG.PUSHALOT_APIKEY, - "pushover_enabled": checked(plexpy.CONFIG.PUSHOVER_ENABLED), - "pushover_keys": plexpy.CONFIG.PUSHOVER_KEYS, - "pushover_apitoken": plexpy.CONFIG.PUSHOVER_APITOKEN, - "pushover_priority": plexpy.CONFIG.PUSHOVER_PRIORITY, - "pushbullet_enabled": checked(plexpy.CONFIG.PUSHBULLET_ENABLED), - "pushbullet_apikey": plexpy.CONFIG.PUSHBULLET_APIKEY, - "pushbullet_deviceid": plexpy.CONFIG.PUSHBULLET_DEVICEID, - "twitter_enabled": checked(plexpy.CONFIG.TWITTER_ENABLED), - "osx_notify_enabled": checked(plexpy.CONFIG.OSX_NOTIFY_ENABLED), - "osx_notify_app": plexpy.CONFIG.OSX_NOTIFY_APP, - "boxcar_enabled": checked(plexpy.CONFIG.BOXCAR_ENABLED), - "boxcar_token": plexpy.CONFIG.BOXCAR_TOKEN, "cache_sizemb": plexpy.CONFIG.CACHE_SIZEMB, - "email_enabled": checked(plexpy.CONFIG.EMAIL_ENABLED), - "email_from": plexpy.CONFIG.EMAIL_FROM, - "email_to": plexpy.CONFIG.EMAIL_TO, - "email_smtp_server": plexpy.CONFIG.EMAIL_SMTP_SERVER, - "email_smtp_user": plexpy.CONFIG.EMAIL_SMTP_USER, - "email_smtp_password": plexpy.CONFIG.EMAIL_SMTP_PASSWORD, - "email_smtp_port": int(plexpy.CONFIG.EMAIL_SMTP_PORT), - "email_tls": checked(plexpy.CONFIG.EMAIL_TLS), "pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER, "pms_ip": plexpy.CONFIG.PMS_IP, "pms_logs_folder": plexpy.CONFIG.PMS_LOGS_FOLDER, @@ -421,7 +412,6 @@ class WebInterface(object): "pms_ssl": checked(plexpy.CONFIG.PMS_SSL), "pms_use_bif": checked(plexpy.CONFIG.PMS_USE_BIF), "pms_uuid": plexpy.CONFIG.PMS_UUID, - "plexwatch_database": plexpy.CONFIG.PLEXWATCH_DATABASE, "date_format": plexpy.CONFIG.DATE_FORMAT, "time_format": plexpy.CONFIG.TIME_FORMAT, "grouping_global_history": checked(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY), @@ -463,6 +453,8 @@ class WebInterface(object): "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE), "home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT, + "home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS, + "home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS, "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, "buffer_wait": plexpy.CONFIG.BUFFER_WAIT } @@ -474,12 +466,7 @@ class WebInterface(object): # Handle the variable config options. Note - keys with False values aren't getting passed checked_configs = [ - "launch_browser", "enable_https", "api_enabled", "freeze_db", "growl_enabled", - "prowl_enabled", "xbmc_enabled", "check_github", - "plex_enabled", "nma_enabled", "pushalot_enabled", - "pushover_enabled", "pushbullet_enabled", - "twitter_enabled", "osx_notify_enabled", - "boxcar_enabled", "email_enabled", "email_tls", + "launch_browser", "enable_https", "api_enabled", "freeze_db", "check_github", "grouping_global_history", "grouping_user_history", "grouping_charts", "pms_use_bif", "pms_ssl", "tv_notify_enable", "movie_notify_enable", "music_notify_enable", "tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start", @@ -515,6 +502,14 @@ class WebInterface(object): if kwargs['pms_ip'] != plexpy.CONFIG.PMS_IP: refresh_users = True + if 'home_stats_cards' in kwargs: + if kwargs['home_stats_cards'] != 'watch_statistics': + kwargs['home_stats_cards'] = ', '.join(kwargs['home_stats_cards']) + + if 'home_library_cards' in kwargs: + if kwargs['home_library_cards'] != 'library_statistics': + kwargs['home_library_cards'] = ', '.join(kwargs['home_library_cards']) + plexpy.CONFIG.process_kwargs(kwargs) # Write the config @@ -535,15 +530,6 @@ class WebInterface(object): @cherrypy.expose def set_notification_config(self, **kwargs): - # Handle the variable config options. Note - keys with False values aren't getting passed - - checked_configs = [ - "email_tls" - ] - for checked_config in checked_configs: - if checked_config not in kwargs: - # checked items should be zero or one. if they were not sent then the item was not checked - kwargs[checked_config] = 0 for plain_config, use_config in [(x[4:], x) for x in kwargs if x.startswith('use_')]: # the use prefix is fairly nice in the html, but does not match the actual config @@ -1112,6 +1098,18 @@ class WebInterface(object): else: logger.warn('Unable to retrieve data.') + @cherrypy.expose + def get_server_children(self, **kwargs): + + pms_connect = pmsconnect.PmsConnect() + result = pms_connect.get_server_children() + + if result: + cherrypy.response.headers['Content-type'] = 'application/json' + return json.dumps(result) + else: + logger.warn('Unable to retrieve data.') + @cherrypy.expose def get_activity(self, **kwargs):