diff --git a/.gitignore b/.gitignore index 3e58c483..cb7eb12f 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ cache/* *.csr *.pem +# Mergetool +*.orgin + # OS generated files # ###################### .DS_Store? @@ -32,7 +35,7 @@ Icon? Thumbs.db #Ignore files generated by PyCharm -.idea/* +*.idea/* #Ignore files generated by vi *.swp diff --git a/CHANGELOG.md b/CHANGELOG.md index 446f639b..30a3279a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,33 @@ # Changelog +## v1.3.7 (2016-02-20) + +* Fix: Verifying server with SSL enabled. +* Fix: Regression where {stream_duration} reported as 0. +* Fix: Video metadata flags showing up for track info. +* Fix: Custom library icons not applied to Library Statistics. +* Fix: Typos in the Web UI. +* Add: ETA to Current Activity overlay. +* Add: Total duration to Libraries and Users tables. +* Add: {machine_id} to notification options. +* Add: IMDB, TVDB, TMDb, Last.fm, and Trackt IDs/URLs to notification options. +* Add: {poster_url} to notification options using Imgur. +* Add: Poster and link for Facebook notifications. +* Add: Log javascript errors from the Web UI. +* Add: Configuration and Scheduler info to the settings page. +* Add: Schedule background task to backup the PlexPy database. +* Add: URL anonymizer for external links. +* Add: Plex Media Scanner log file to Log viewer. +* Add: API v2 (sill very experimental) (Thanks @Hellowlol) +* Change: Allow secure websocket connections. +* Change: History grouping now accounts for the view offset. +* Change: Subject line can be toggled off for Facebook, Slack, Telegram, and Twitter. +* Change: Create self-signed SSL certificates when enabling HTTPS. +* Change: Revert homepage "Last Played" to "Last Watched". +* Change: Disable monitor remote access checkbox if remote access is not enabled on the PMS. +* Change: Disable IP logging checkbox if PMS version is 0.9.14 or greater. + + ## v1.3.6 (2016-02-03) * Fix: Regression where {duration} not reported in minutes. diff --git a/data/interfaces/default/css/plexpy.css b/data/interfaces/default/css/plexpy.css index ad0a0c8d..e200da78 100644 --- a/data/interfaces/default/css/plexpy.css +++ b/data/interfaces/default/css/plexpy.css @@ -501,7 +501,8 @@ textarea.form-control:focus { .libraries-poster-face { overflow: hidden; float: left; - background-size: contain; + background-size: cover; + background-position: center; height: 40px; width: 40px; /*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); @@ -1717,7 +1718,8 @@ a:hover .item-children-poster { float: left; margin-top: 15px; margin-right: 15px; - background-size: contain; + background-size: cover; + background-position: center; height: 80px; width: 80px; /*-webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); @@ -2178,6 +2180,10 @@ a .home-platforms-instance-list-oval:hover, .refresh-libraries-button { float: right; } +.refresh-users-button, +.refresh-libraries-button { + margin-right: 5px; +} .nav-settings, .nav-settings ul { margin: 0px 0px 20px 0px; @@ -2712,4 +2718,44 @@ table[id^='media_info_child'] table[id^='media_info_child'] thead th { } .selectize-input input[type='text'] { height: 20px; +} +.small-muted { + font-size: small; + color: #777; +} +.config-info-table, +.config-scheduler-table { + width: 100% +} +.config-info-table td, +.config-info-table th, +.config-scheduler-table td, +.config-scheduler-table th { + padding-bottom: 5px; +} +.config-info-table td:first-child { + width: 150px; +} +.config-scheduler-table td:first-child { + width: 225px; +} +.config-scheduler-table th { + color: #fff; +} +a.no-highlight { + color: #777; +} +a.no-highlight:hover { + color: #fff; +} +.top-line { + border-top: 1px dotted #777; + padding-top: 5px; +} +.help-bold { + font-weight: bold; + color: #fff; +} +.save-button { + margin-top: 15px; } \ No newline at end of file diff --git a/data/interfaces/default/current_activity.html b/data/interfaces/default/current_activity.html index 5b4ab21e..47be0dcd 100644 --- a/data/interfaces/default/current_activity.html +++ b/data/interfaces/default/current_activity.html @@ -198,6 +198,13 @@ DOCUMENTATION :: END % else: IP: N/A % endif +
+ ETA: + + +
${a['view_offset']}/${a['duration']} diff --git a/data/interfaces/default/home_stats.html b/data/interfaces/default/home_stats.html index eb48bee8..de0995ea 100644 --- a/data/interfaces/default/home_stats.html +++ b/data/interfaces/default/home_stats.html @@ -692,7 +692,7 @@ DOCUMENTATION :: END
  • -

    Last Played

    +

    Last Watched

    diff --git a/data/interfaces/default/info.html b/data/interfaces/default/info.html index 6786b307..9a025348 100644 --- a/data/interfaces/default/info.html +++ b/data/interfaces/default/info.html @@ -171,10 +171,10 @@ DOCUMENTATION :: END % endif % if data['media_type'] == 'movie' or data['media_type'] == 'episode' or data['media_type'] == 'track':
    - % if data['video_codec']: + % if data['media_type'] != 'track' and data['video_codec']: % endif - % if data['video_resolution']: + % if data['media_type'] != 'track' and data['video_resolution']: % endif % if data['audio_codec']: diff --git a/data/interfaces/default/js/script.js b/data/interfaces/default/js/script.js index 5d82f90c..5eac0f22 100644 --- a/data/interfaces/default/js/script.js +++ b/data/interfaces/default/js/script.js @@ -54,7 +54,7 @@ function showMsg(msg,loader,timeout,ms,error) { } } -function doAjaxCall(url,elem,reload,form) { +function doAjaxCall(url, elem, reload, form, callback) { // Set Message feedback = $("#ajaxMsg"); update = $("#updatebar"); @@ -157,6 +157,9 @@ function doAjaxCall(url,elem,reload,form) { complete: function(jqXHR, textStatus) { // Remove loaders and stuff, ajax request is complete! loader.remove(); + if (typeof callback === "function") { + callback(); + } } }); } @@ -252,13 +255,13 @@ function isPrivateIP(ip_address) { function humanTime(seconds) { if (seconds >= 86400) { - text = '

    ' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + - '

    days

    ' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + - '

    hrs

    ' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '

    mins

    '; + text = '

    ' + Math.floor(moment.duration(seconds, 'seconds').asDays()) + '

    days

    ' + + '

    ' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '

    hrs

    ' + + '

    ' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '

    mins

    '; return text; } else if (seconds >= 3600) { - text = '

    ' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + - '

    hrs

    ' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '

    mins

    '; + text = '

    ' + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + '

    hrs

    ' + + '

    ' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '

    mins

    '; return text; } else if (seconds >= 60) { text = '

    ' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '

    mins

    '; @@ -269,6 +272,25 @@ function humanTime(seconds) { } } +function humanTimeClean(seconds) { + if (seconds >= 86400) { + text = Math.floor(moment.duration(seconds, 'seconds').asDays()) + ' days ' + + Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' + + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; + return text; + } else if (seconds >= 3600) { + text = Math.floor(moment.duration((seconds % 86400), 'seconds').asHours()) + ' hrs ' + + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; + return text; + } else if (seconds >= 60) { + text = Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + ' mins'; + return text; + } else { + text = '0'; + return text; + } +} + String.prototype.toProperCase = function () { return this.replace(/\w\S*/g, function(txt){return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();}); }; @@ -372,3 +394,16 @@ function clearSearchButton(tableName, table) { table.search('').draw(); }); } + +// Taken from https://github.com/Hellowlol/HTPC-Manager +window.onerror = function (message, file, line) { + var e = { + 'page': window.location.href, + 'message': message, + 'file': file, + 'line': line + }; + + $.post("log_js_errors", e, function (data) { + }); +}; \ No newline at end of file diff --git a/data/interfaces/default/js/tables/libraries.js b/data/interfaces/default/js/tables/libraries.js index 377295ae..46844506 100644 --- a/data/interfaces/default/js/tables/libraries.js +++ b/data/interfaces/default/js/tables/libraries.js @@ -161,12 +161,28 @@ libraries_list_table_options = { $(td).html('n/a'); } }, - "width": "25%", + "width": "18%", "className": "hidden-sm hidden-xs" }, { "targets": [9], "data": "plays", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== null && cellData !== '') { + $(td).html(cellData); + } + }, + "searchable": false, + "width": "7%" + }, + { + "targets": [10], + "data": "duration", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== null && cellData !== '') { + $(td).html(humanTimeClean(cellData)); + } + }, "searchable": false, "width": "10%" } diff --git a/data/interfaces/default/js/tables/users.js b/data/interfaces/default/js/tables/users.js index f7147a17..d1abb6c9 100644 --- a/data/interfaces/default/js/tables/users.js +++ b/data/interfaces/default/js/tables/users.js @@ -165,12 +165,28 @@ users_list_table_options = { $(td).html('n/a'); } }, - "width": "30%", + "width": "23%", "className": "hidden-sm hidden-xs" }, { "targets": [8], "data": "plays", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== null && cellData !== '') { + $(td).html(cellData); + } + }, + "searchable": false, + "width": "7%" + }, + { + "targets": [9], + "data": "duration", + "createdCell": function (td, cellData, rowData, row, col) { + if (cellData !== null && cellData !== '') { + $(td).html(humanTimeClean(cellData)); + } + }, "searchable": false, "width": "10%" } diff --git a/data/interfaces/default/libraries.html b/data/interfaces/default/libraries.html index 1e99697c..41fc9825 100644 --- a/data/interfaces/default/libraries.html +++ b/data/interfaces/default/libraries.html @@ -2,6 +2,7 @@ <%def name="headIncludes()"> + @@ -23,6 +24,7 @@ All Libraries
    + % if config['update_section_ids'] == -1: % else: @@ -48,6 +50,7 @@ Last Accessed Last Played Total Plays + Total Duration @@ -79,6 +82,7 @@ <%def name="javascriptIncludes()"> + @@ -96,6 +100,8 @@ } libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options); + var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: ' Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] }); + $(colvis.button()).appendTo('div.colvis-button-bar'); clearSearchButton('libraries_list_table', libraries_list_table); diff --git a/data/interfaces/default/library.html b/data/interfaces/default/library.html index ee7162b9..3333b53c 100644 --- a/data/interfaces/default/library.html +++ b/data/interfaces/default/library.html @@ -37,7 +37,9 @@ DOCUMENTATION :: END % if data:
    + % if data['library_art']:
    + % endif
    @@ -52,7 +54,7 @@ DOCUMENTATION :: END
    % else: -
    Unable to retrieve data from database. +
    No stats to show.

    % endif \ No newline at end of file diff --git a/data/interfaces/default/library_stats.html b/data/interfaces/default/library_stats.html index 048d5e6f..80af7341 100644 --- a/data/interfaces/default/library_stats.html +++ b/data/interfaces/default/library_stats.html @@ -75,13 +75,13 @@ DOCUMENTATION :: END
    % endif
    - % if library['thumb']: + % if library['thumb'].startswith("http"):
    -
    +
    % else:
    -
    +
    % endif

  • diff --git a/data/interfaces/default/logs.html b/data/interfaces/default/logs.html index ecb1d17f..04a2938a 100644 --- a/data/interfaces/default/logs.html +++ b/data/interfaces/default/logs.html @@ -29,6 +29,7 @@ from plexpy import helpers
    @@ -57,6 +58,19 @@ from plexpy import helpers
    +
    + + + + + + + + + + +
    TimestampLevelMessage
    +
    @@ -98,11 +112,18 @@ from plexpy import helpers function LoadPlexLogs() { plex_log_table_options.ajax = { - "url": "get_plex_log" + "url": "get_plex_log?log_type=server" } plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options); } + function LoadPlexScannerLogs() { + plex_log_table_options.ajax = { + "url": "get_plex_log?log_type=scanner" + } + plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options); + } + $("#plexpy-logs-btn").click(function() { $("#clear-logs").show(); LoadPlexPyLogs(); @@ -115,6 +136,12 @@ from plexpy import helpers clearSearchButton('plex_log_table', plex_log_table); }); + $("#plex-scanner-logs-btn").click(function() { + $("#clear-logs").hide(); + LoadPlexScannerLogs(); + clearSearchButton('plex_scanner_log_table', plex_scanner_log_table); + }); + $("#clear-logs").click(function() { var r = confirm("Are you sure you want to clear the PlexPy log?"); if (r == true) { diff --git a/data/interfaces/default/notification_config.html b/data/interfaces/default/notification_config.html index d071ce77..d1dd0675 100644 --- a/data/interfaces/default/notification_config.html +++ b/data/interfaces/default/notification_config.html @@ -132,7 +132,7 @@ from plexpy import helpers function reloadModal() { $.ajax({ url: 'get_notification_agent_config', - data: { config_id: '${agent["id"]}' }, + data: { agent_id: '${agent["id"]}' }, cache: false, async: true, complete: function (xhr, status) { @@ -147,9 +147,8 @@ from plexpy import helpers }) $('#save-notification-item').click(function () { - doAjaxCall('set_notification_config', $(this), 'tabs', true); // Reload modal to update certain fields - reloadModal(); + doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal); return false; }); @@ -195,7 +194,7 @@ from plexpy import helpers $.ajax({ url: 'test_notifier', data: { - config_id: '${agent["id"]}', + agent_id: '${agent["id"]}', subject: $('#test_subject').val(), body: $('#test_body').val(), script: $('#test_script').val(), @@ -211,8 +210,8 @@ from plexpy import helpers }); $('#pushbullet_apikey, #pushover_apitoken, #scripts_folder').on('change', function () { - doAjaxCall('set_notification_config', $(this), 'tabs', true); - reloadModal(); + // Reload modal to update certain fields + doAjaxCall('set_notification_config', $(this), 'tabs', true, reloadModal); return false; }); diff --git a/data/interfaces/default/scheduler_table.html b/data/interfaces/default/scheduler_table.html new file mode 100644 index 00000000..d1e4d7e0 --- /dev/null +++ b/data/interfaces/default/scheduler_table.html @@ -0,0 +1,64 @@ +<%doc> +USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE + +For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/ + +Filename: scheduler_table.html +Version: 0.1 + +DOCUMENTATION :: END + + +<%! +import arrow +import plexpy +from plexpy import common + +scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()] +%> + + + + + + + + + + + + + % for job in common.SCHEDULER_LIST: + % if job in scheduled_jobs: + <% + sched_job = plexpy.SCHED.get_job(job) + run_interval = arrow.get(str(sched_job.trigger.interval), ['H:mm:ss', 'HH:mm:ss']) + next_run_interval = arrow.get(sched_job.next_run_time).timestamp - arrow.now().timestamp + %> + + + + + + + + % elif job == 'Check for active sessions' and plexpy.CONFIG.MONITORING_USE_WEBSOCKET and not plexpy.POLLING_FAILOVER: + + + + + + + + % else: + + + + + + + + % endif + % endfor + +
    Scheduled TaskStateIntervalNext Run InNext Run Time
    ${sched_job.id} Active${arrow.get(run_interval).format('HH:mm:ss')}${arrow.get(next_run_interval).format('HH:mm:ss')}${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}
    ${job} Using WebsocketN/AN/AN/A
    ${job} InactiveN/AN/AN/A
    \ No newline at end of file diff --git a/data/interfaces/default/settings.html b/data/interfaces/default/settings.html index bbcec5f6..36b82fb4 100644 --- a/data/interfaces/default/settings.html +++ b/data/interfaces/default/settings.html @@ -1,7 +1,9 @@ <%inherit file="base.html"/> <%! +import sys import plexpy from plexpy import notifiers, common, versioncheck +from plexpy.helpers import anon_url available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['name']) %> @@ -33,7 +35,8 @@ available_notification_agents = sorted(notifiers.available_notification_agents()
    +
    + +
    +
    + +
    +
    +

    Backlink protection via anonymizer service, must end in "?".

    +

    PlexWatch Import Tool

    @@ -490,13 +613,14 @@ available_notification_agents = sorted(notifiers.available_notification_agents() Use Websocket (requires restart) [experimental]

    Instead of polling the server at regular intervals let the server tell us when something happens.
    - This is currently experimental. Encrypted websocket is not currently supported.

    + This is currently experimental.

    -

    Enable to have PlexPy check if remote access to the Plex Media Server goes down. Your server needs to have remote access enabled.

    + +

    Enable to have PlexPy check if remote access to the Plex Media Server goes down.

    @@ -534,7 +658,7 @@ available_notification_agents = sorted(notifiers.available_notification_agents()

    - Enable this to attempt to log the IP address of the user (for PMS 0.9.12 and below, IP address is automatically logged for PMS 0.9.14 and above). + Enable this to attempt to log the IP address of the user.

    @@ -639,11 +763,11 @@ available_notification_agents = sorted(notifiers.available_notification_agents()

    You can set custom formatted text for each type of notification. - Click here for a list of available parameters which can be used. + Click here for a list of available parameters which can be used.

    - You can also add tags to exclude certain text depending on the media type. Click - here to view usage information. + You can also add tags to exclude certain text depending on the media type. + Click here to view usage information.