Compare commits

...

57 Commits

Author SHA1 Message Date
drzoidberg33
0f04a6d25b Merge pull request #110 from drzoidberg33/revert-108-info-pages
Revert "Info pages"
2015-08-23 00:09:29 +02:00
drzoidberg33
2d19accdd1 Revert "Info pages" 2015-08-23 00:08:55 +02:00
drzoidberg33
3d5706002d Merge pull request #108 from JonnyWong16/info-pages
Info pages
2015-08-23 00:08:50 +02:00
Tim
abec036cb2 v1.1.3 2015-08-22 17:11:04 +02:00
drzoidberg33
8bdd40f011 Merge pull request #105 from JonnyWong16/history-tooltips
History tooltips
2015-08-22 16:32:52 +02:00
drzoidberg33
b98c17e738 Merge pull request #104 from JonnyWong16/stream_duration-notification
Stream duration notification option
2015-08-22 16:32:35 +02:00
Jonathan Wong
a71c6e6592 Fix Air date and runtime 2015-08-22 05:24:06 -07:00
Jonathan Wong
545b7cb581 Remove grandparent_rating_key from season media type 2015-08-22 05:24:02 -07:00
Jonathan Wong
0848e317ee Add links to info pages to move between show / season / episode
* Clicking poster moves up one level
* Links in titles of season and episodes
2015-08-22 05:18:33 -07:00
Jonathan Wong
e976e6cf5c Added more detail to history items
* Year for movies
* Season and episode number for episodes
* Album name for tracks
2015-08-22 05:17:30 -07:00
Jonathan Wong
95cd2b4f81 Forgot semicolon 2015-08-22 03:13:23 -07:00
Jonathan Wong
3df73dc287 Implement changes to history table to user tables
* Add tooltips
* Change icons
2015-08-22 01:20:57 -07:00
Jonathan Wong
3a703eb605 Move icons to the left
* Added icons for "direct play" and "direct stream"
* Separate icons for "movie" and "episode"
2015-08-22 01:20:11 -07:00
Jonathan Wong
e94c00ca48 Fix history table on info pages 2015-08-22 01:17:58 -07:00
Jonathan Wong
d6a34b3e6b Get thumbnails for tooltips in history table 2015-08-21 03:56:47 -07:00
Jonathan Wong
3cbf05d54a Update settings page to show available stream_duration 2015-08-21 03:07:18 -07:00
Jonathan Wong
cba8608c23 Adds stream_duration to notification options 2015-08-21 02:59:49 -07:00
Jonathan Wong
e34865d0dd Initial implementation of tooltips in history table
* Move transcode icon to platform column
* Tooltips for transcode icon and media type icon
* Popover for album art when hover over title
2015-08-20 21:55:11 -07:00
drzoidberg33
b2a7f639bb Merge pull request #100 from JonnyWong16/dev
Some more changes
2015-08-20 22:10:11 +02:00
JonnyWong16
fcbc921470 Fix delete history row only if the button is clicked
History row was deleting even when clicking in the table cell outside
the button.
2015-08-20 12:13:54 -07:00
JonnyWong16
5168d76e86 Add "Last Platform" and "Last Watched" to user data tables 2015-08-20 12:13:52 -07:00
JonnyWong16
968d213b97 Fix platform text clickable and swap column order
Other minor changes to history tables:
* hidden columns with  smaller screen size
* font colour
2015-08-20 12:13:51 -07:00
JonnyWong16
9fc4573c42 Show 0 mins with 0 plays on user page. 2015-08-20 12:13:49 -07:00
drzoidberg33
9adf5cc39a Merge pull request #96 from JonnyWong16/dev
More stylized homepage statistics and other style changes
2015-08-19 23:29:58 +02:00
JonnyWong16
2ca04f4a8b Missed a file in last commit 2015-08-19 13:54:11 -07:00
JonnyWong16
fd3b2a48f9 Revert home stats poster size. 2015-08-19 13:53:23 -07:00
JonnyWong16
01b3ae377b Add clear search button to data tables. 2015-08-19 13:25:29 -07:00
JonnyWong16
5a1516286c Changes to info pages
* Style changes
* Added missing metadata
* Don't show metadata field if data is unavailable
2015-08-19 01:13:55 -07:00
JonnyWong16
317a9f0b8e Dynamically adjust user recently watched items
Only show one row of recently watched items which adjusts to the window
size, similar to recently added items.
2015-08-19 00:51:27 -07:00
JonnyWong16
c98505038a Fallback to show poster if season poster is unavailable. 2015-08-19 00:50:19 -07:00
JonnyWong16
1ec1edefdd More style changes across site 2015-08-18 19:05:14 -07:00
JonnyWong16
aa351bd965 Fixed graphs days text not updating with cookies. 2015-08-18 17:05:34 -07:00
JonnyWong16
519ff6b203 Fixed default Gravatar on homepage. 2015-08-18 16:47:49 -07:00
JonnyWong16
5b2beb2b81 More stylized homepage statistics
Also fixed some formatting on user page and posters on info page.
2015-08-18 16:44:17 -07:00
Tim
13e6a70a30 Fix typo on user edit page.
Change styling of version number and changelog button.
2015-08-19 00:03:49 +02:00
Tim
7e99eb7a2a Update README. 2015-08-18 23:39:20 +02:00
Tim
6efaabb630 Remove some more Headphones references. 2015-08-18 23:27:09 +02:00
Tim
2536fdf17b Fix month display showing "invalid date" on totals graph.
Make duration on home stats human readable.
2015-08-18 22:59:24 +02:00
drzoidberg33
7e8a427107 Merge pull request #95 from JonnyWong16/dev
Update to homepage statistics
2015-08-18 22:50:29 +02:00
JonnyWong16
bbaf428fd8 Fix "Select columns" button text 2015-08-18 13:41:34 -07:00
JonnyWong16
7dfd063138 Update documentation for home stats 2015-08-18 12:36:10 -07:00
JonnyWong16
5c94b21bd1 Update to homepage statistics
* Added most popular movie to homepage
* New setting to toggle statistics based on play duration instead of
play count
2015-08-18 12:32:41 -07:00
drzoidberg33
58474d9565 Merge pull request #87 from JonnyWong16/dev
Use cookies to save graph state.
2015-08-18 13:36:30 +02:00
JonnyWong16
6cb1c057cf Another update to recently added and recently watched.
Recently added albums now include artist name.
Recently watched episodes now include show name.
2015-08-18 02:26:52 -07:00
JonnyWong16
22cc06dec3 Fixed tab spacing 2015-08-17 16:25:27 -07:00
JonnyWong16
357797df6b Use cookies to save graph state.
The selected graph state is remembered on refreshing the graph page.
2015-08-17 16:23:45 -07:00
Tim
4c6f6ca736 Clearer version info in Settings menu.
Rudimentary changelog reader (Settings menu -> General)
Reverse the changelog order - newer changes first.
2015-08-18 01:05:12 +02:00
drzoidberg33
c0214f1489 Merge pull request #83 from JonnyWong16/dev
Fixed recently added and recently watched posters.
2015-08-17 22:38:31 +02:00
JonnyWong16
dd27f9bf72 Fixed recently added and recently watched posters.
Changed posters to match Plex/Web style, and fixed stretching on "home
video" posters.
2015-08-17 13:03:44 -07:00
Tim
c1c7911d08 Fix IP modal on history tab of user page. 2015-08-17 21:21:02 +02:00
drzoidberg33
755e9107fa Merge pull request #79 from JonnyWong16/dev
Two digit season and episode numbers and FreeNAS init script
2015-08-17 11:19:42 +02:00
JonnyWong16
3bb6320fc1 Modified FreeBSD init script for FreeNAS 2015-08-16 23:25:14 -07:00
JonnyWong16
b5ad88ae5a Two digit season and episode numbers for notifications. 2015-08-16 23:24:52 -07:00
Tim
bbcf3bf7da Don't reset to page 1 on history row delete. 2015-08-16 23:45:17 +02:00
Tim
51e1949538 Hide delete and watched columns in column selector.
Add delete mode to user history tab too.
Some styling changes.
2015-08-16 23:30:45 +02:00
Tim
6b1a57e650 Add "delete mode" on history table allows individual rows to be deleted permanently.
Add user history purge option in edit user screen. Will remove all history for selected user.
2015-08-16 22:52:08 +02:00
Tim
8e57df53fd Report the correct version numbers. 2015-08-16 13:48:39 +02:00
40 changed files with 2284 additions and 1182 deletions

View File

@@ -1,13 +1,34 @@
# Changelog
## v1.0 (2015-08-11)
## v1.1.3 (2015-08-22)
* First release
* Show human readable version info and this cool changelog in Settings -> General.
* Add a "delete" mode to the history tables. Toggle it to show a delete button next to each history item.
* Two digit season and episode numbers for custom notification messages. Thanks @JohnnyWong.
* New FreeNAS init script. Thanks @JohnnyWong.
* Lots of styling improvements! Thanks @JohnnyWong.
* Graph page remembers last selected options. Thanks @JohnnyWong.
* New Popular movie homepage stats. Thanks @JohnnyWong.
* Add option for duration vs play count on home stats. (Settings -> Extra Settings). Thanks @JohnnyWong.
* Clean up media info pages. Don't show metadata that is missing. Thanks @JohnnyWong.
* Add clear button to search inputs. Thanks @JohnnyWong.
* New columns on Users list. Thanks @JohnnyWong.
* New stream duration option for custom notification messages. Thanks @JohnnyWong.
* Rad new tooltips on the history pages. Thanks @JohnnyWong.
* And a lot of small visual changes and fixes. Thanks @JohnnyWong.
* Fixed IP address modal on user history page.
* Fixed "invalid date" showing on monthly plays graph.
## v1.0.1 (2015-08-13)
## v1.1.2 (2015-08-16)
* Allow SSL certificate check override for certain systems with bad CA stores.
* Fix typo on graphs page causing date selection to break on Safari.
* Fix bug where user refresh would fail under certain circumstances.
## v1.1.1 (2015-08-15)
* Added Most watched movie for home stats. Thanks @jroyal.
* Added TV show title to recently added text. Thanks @jroyal.
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
* Fix bug where custom avatar URL would get reset on every user refresh.
## v1.1.0 (2015-08-15)
@@ -24,13 +45,11 @@
* Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close.
* Fix some styling niggles.
## v1.1.1 (2015-08-15)
## v1.0.1 (2015-08-13)
* Added Most watched movie for home stats. Thanks @jroyal.
* Added TV show title to recently added text. Thanks @jroyal.
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
* Fix bug where custom avatar URL would get reset on every user refresh.
* Allow SSL certificate check override for certain systems with bad CA stores.
* Fix typo on graphs page causing date selection to break on Safari.
## v1.1.2 (2015-08-16)
## v1.0 (2015-08-11)
* Fix bug where user refresh would fail under certain circumstances.
* First release

View File

@@ -6,7 +6,7 @@ This project is based on code from Headphones (https://github.com/rembo10/headph
* plexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
If you'd like to buy me a beer, hit the donate button below.
If you'd like to buy me a beer, hit the donate button below. All donations go to the project maintainer (primarily for the procurement of liquid refreshment).
[![Donate](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=G9HZK9BDJLKT6)
@@ -34,6 +34,14 @@ If you'd like to buy me a beer, hit the donate button below.
* stream type (direct, transcoded)
* video type & resolution
* audio type & channel count.
* Top statistics on home page with configurable duration and measurement metric:
* Most watched TV
* Most popular TV
* Most watched Movie
* Most popular Movie
* Most active user
* Most active platform
* Recently added media and how long ago it was added
@@ -41,42 +49,49 @@ If you'd like to buy me a beer, hit the donate button below.
* date
* user
* platform
* ip address (if enabled in plexWatch)
* ip address
* title
* stream information details
* start time
* paused duration length
* stop time
* duration length
* percentage completed
* watched progress
* show/hide columns
* delete mode - allows deletion of specific history items
* Full user list with general information and comparison stats
* Individual user information
- username and gravatar (if available)
- daily, weekly, monthly, all time stats for play count and duration length
- individual platform stats for each user
- public ip address history with last seen date and geo tag location
- recently watched content
- watching history
- synced items
* username and gravatar (if available)
* daily, weekly, monthly, all time stats for play count and duration length
* individual platform stats for each user
* public ip address history with last seen date and geo tag location
* recently watched content
* watching history
* synced items
* assign users custom friendly names within PlexPy
* assign users custom avatar URL within PlexPy
* disable history logging per user
* disable notifications per user
* option to purge all history per user.
* Rich analytics presented using Highcharts graphing
- user-selectable time periods of 30, 90 or 365 days
- daily watch count and duration
- totals by day of week and hours of the day
- totals by top 10 platform
- totals by top 10 users
- detailed breakdown by transcode decision
- source and stream resolutions
- transcode decision counts by user and platform
- total monthly counts
* user-selectable time periods of 30, 90 or 365 days
* daily watch count and duration
* totals by day of week and hours of the day
* totals by top 10 platform
* totals by top 10 users
* detailed breakdown by transcode decision
* source and stream resolutions
* transcode decision counts by user and platform
* total monthly counts
* Content information pages
- movies (includes watching history)
- tv shows (includes watching history)
- tv seasons
- tv episodes (includes watching history)
* movies (includes watching history)
* tv shows (includes watching history)
* tv seasons
* tv episodes (includes watching history)
* Full sync list data on all users syncing items from your library

View File

@@ -75,6 +75,7 @@ ul.ColVis_collection {
background-color: #444;
overflow: hidden;
z-index: 2002;
border-radius: 4px;
}
ul.ColVis_collection li {

File diff suppressed because it is too large Load Diff

View File

@@ -58,6 +58,12 @@ DOCUMENTATION :: END
</label>
<p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p>
</div>
% if data['user_id']:
<div class="form-group">
<button class="btn btn-danger" id="delete-all-history">Purge</button>
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
</div>
% endif
</fieldset>
</div>
<div class="modal-footer">
@@ -112,6 +118,21 @@ DOCUMENTATION :: END
});
% endif
});
$("#delete-all-history").click(function() {
var r = confirm("Are you REALLY REALLY REALLY sure you want to delete all history for this user?");
if (r == true) {
$.ajax({
url: 'delete_all_user_history',
data: {user_id: '${data['user_id']}'},
cache: false,
async: true,
success: function(data) {
location.reload();
}
});
}
});
</script>
% endif

View File

@@ -263,9 +263,36 @@
<script>
$(document).ready(function () {
var current_range = 30;
// Save graph state to cookies
$('input[name=yaxis-options]').change(function() {
setCookie('graphType', $(this).val(), 365, '/');
});
$('input[name=date-options]').change(function() {
setCookie('graphDate', $(this).val(), 365, '/');
});
$('a[data-toggle=tab]').click(function() {
setCookie('graphTab', $(this).attr('href'), 365, '/');
});
// Initial values for graph if no saved state
var yaxis = 'plays';
var current_range = 30;
var current_tab = '#tabs-1';
// Read saved graph state from cookies and set initial values
if(getCookie('graphType')) {
var yaxis = getCookie('graphType');
$('input[name=yaxis-options][value=' + yaxis + ']').prop('checked', true).trigger('click');
}
if(getCookie('graphDate')) {
var current_range = getCookie('graphDate');
$('input[name=date-options][value=' + current_range + ']').prop('checked', true).trigger('click');
$('.days').html(current_range);
}
if(getCookie('graphTab')) {
var current_tab = getCookie('graphTab');
$('a[data-toggle=tab][href=' + current_tab + ']').trigger('click');
}
function loadGraphsTab1(time_range, yaxis) {
setGraphFormat(yaxis);
@@ -432,12 +459,8 @@
data: { y_axis: yaxis },
dataType: "json",
success: function(data) {
var dateArray = [];
for (var i = 0; i < data.categories.length; i++) {
dateArray.push(moment(data.categories[i], 'YYYY-MM').format('MMM YYYY'));
}
hc_plays_by_month_options.yAxis.min = 0;
hc_plays_by_month_options.xAxis.categories = dateArray;
hc_plays_by_month_options.xAxis.categories = data.categories;
hc_plays_by_month_options.series = data.series;
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
}

View File

@@ -13,23 +13,26 @@
<div class="header-bar">
<span><i class="fa fa-history"></i> History</span>
</div>
<div class="colvis-button-bar hidden-xs">
<div class="button-bar">
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete mode</button>&nbsp
<div class="colvis-button-bar hidden-xs"></div>
</div>
</div>
<div class='table-card-back'>
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete_row">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody>
@@ -60,8 +63,22 @@
}
}
history_table = $('#history_table').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table);
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
});
</script>

View File

@@ -43,6 +43,8 @@
}
history_table = $('#history_table').DataTable(history_table_modal_options);
clearSearchButton('history_table', history_table);
});
</script>
% else:

View File

@@ -9,7 +9,8 @@ Variable names: data [array]
data[array_index] :: Usable parameters
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'popular_tv', 'top_user' or 'top_platform'
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'top_movies', 'popular_tv', 'popular_movies', 'top_user' or 'top_platform'
data['stat_type'] Returns the type of the stat. Either 'total_plays' or 'total_duration'
data['rows'] Returns an array containing stat data
data[array_index]['rows'] :: Usable parameters
@@ -21,10 +22,11 @@ grandparent_thumb Returns location of the item's thumbnail. Use with pms_i
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_user' or 'top_platform' ==
== Only if 'stat_id' is 'top_tv' or 'top_movies' 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' ==
== Only of 'stat_id' is 'popular_tv' or 'popular_movies' ==
users_watched Returns the count for the associated stat.
== Only if 'stat_id' is 'top_user' ==
@@ -39,6 +41,22 @@ platform_type Returns the platform name for the associated stat.
DOCUMENTATION :: END
</%doc>
<%!
from plexpy import helpers
# Human readable duration
def hd(minutes):
if int(minutes) > 60:
hours = int(helpers.cast_to_float(minutes) / 60)
minutes = int(helpers.cast_to_float(minutes) % hours)
if minutes > 0:
return "<h3>" + str(hours) + "</h3><p>hrs</p><h3>" + str(minutes) + "</h3><p>mins</p>"
else:
return "<h3>" + str(hours) + "</h3><p>hrs</p>"
else:
return "<h3>" + minutes + "</h3><p>mins</p>"
%>
% if data:
% if data[0]['rows'] or data[2]['rows']:
<ul class="list-unstyled">
@@ -46,132 +64,189 @@ DOCUMENTATION :: END
% if a['stat_id'] == 'top_tv' and a['rows']:
<div class="home-platforms-instance">
<li>
<span>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb']:
<img class="home-platforms-instance-poster"
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Watched TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
${a['rows'][0]['total_duration'] | hd}
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Watched TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
</div>
</div>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb']:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
</li>
</div>
% elif a['stat_id'] == 'popular_tv' and a['rows']:
<div class="home-platforms-instance">
<li>
<span>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb'] != '':
<img class="home-platforms-instance-poster"
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
% else:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Popular TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['users_watched']}</h3>
<p> users</p>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Popular TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['users_watched']}</h3>
<p> users</p>
</div>
</div>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb'] != '':
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
</li>
</div>
% elif a['stat_id'] == 'top_movies' and a['rows']:
<div class="home-platforms-instance">
<li>
<span>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['thumb']:
<img class="home-platforms-instance-poster"
src="pms_image_proxy?img=${a['rows'][0]['thumb']}&width=162&height=240&fallback=poster">
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Watched Movie</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
${a['rows'][0]['total_duration'] | hd}
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Watched Movie</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['thumb']:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
</li>
</div>
% elif a['stat_id'] == 'popular_movies' and a['rows']:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Popular Movie</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['users_watched']}</h3>
<p> users</p>
</div>
</div>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['thumb']:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
</li>
</div>
% elif a['stat_id'] == 'top_users' and a['rows']:
<div class="home-platforms-instance">
<li>
<span>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
% endif
% if a['rows'][0]['thumb'] != '':
<img class="home-platforms-instance-oval" src="${a['rows'][0]['thumb']}"
class="poster-face">
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Active User</h4>
<h5>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% else:
<img class="home-platforms-instance-oval"
src="interfaces/default/images/gravatar-default.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Active User</h4>
<h5>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
<a href="user?user=${a['rows'][0]['user']}">
% endif
${a['rows'][0]['friendly_name']}
</a>
</h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
</a>
</h5>
</div>
<div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
% endif
% if a['rows'][0]['thumb'] != '':
<div class="home-platforms-instance-poster">
<div class="home-platforms-instance-oval" style="background-image: url(${a['rows'][0]['thumb']});">
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="home-platforms-instance-oval" style="background-image: url(interfaces/default/images/gravatar-default.png);">
</div>
% endif
</a>
</li>
</div>
% elif a['stat_id'] == 'top_platforms' and a['rows']:
<div class="home-platforms-instance">
<li>
<div id="platform-stat">
<img class="home-platforms-instance-box" src="interfaces/default/images/platforms/default.png">
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Active Platform</h4>
<h5>${a['rows'][0]['platform_type']}</h5>
</div>
<div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
<div class="home-platforms-instance-name">
<h4>Most Active Platform</h4>
<h5>${a['rows'][0]['platform_type']}</h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
<div id="platform-stat" class="home-platforms-instance-poster">
<div class="home-platforms-instance-box" style="background-image: url(interfaces/default/images/platforms/default.png);">
</div>
</li>
</div>
<script>
$("#platform-stat").html("<img class='home-platforms-instance-box' src='" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + "'>");
$("#platform-stat").html("<div class='home-platforms-instance-box' style='background-image: url(" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + ");'>");
</script>
% endif
% endfor

View File

@@ -21,7 +21,7 @@
<div class="padded-header">
<h3>Statistics <small>Last ${config['home_stats_length']} days</small></h3>
</div>
<div id="home-stats" class="user-platforms">
<div id="home-stats" class="home-platforms">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
<br>
</div>
@@ -45,12 +45,12 @@
<script src="interfaces/default/js/moment-with-locale.js"></script>
<script>
function getHomeStats(days) {
function getHomeStats(days, plays) {
$.ajax({
url: 'home_stats',
cache: false,
async: true,
data: {time_range: days},
data: {time_range: days, stat_type: plays},
complete: function(xhr, status) {
$("#home-stats").html(xhr.responseText);
}
@@ -110,7 +110,7 @@
});
});
getHomeStats(${config['home_stats_length']});
getHomeStats(${config['home_stats_length']}, ${config['home_stats_type']});
</script>

View File

@@ -30,6 +30,7 @@ genres Returns an array of genres.
actors Returns an array of actors.
directors Returns an array of directors.
studio Returns the name of the studio.
originally_available_at Returns the air date of the item.
DOCUMENTATION :: END
</%doc>
@@ -53,46 +54,58 @@ DOCUMENTATION :: END
<div class="row">
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['type'] == 'episode':
<img src="pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster">
% if data['type'] == 'episode' and data['parent_thumb']:
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster);"></div>
% elif data['type'] == 'episode':
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
% else:
<img src="pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"></div>
% endif
</div>
<div class="summary-content">
<div class="summary-content-title">
% if data['type'] == 'movie':
<h1>${data['title']} (${data['year']})</h1>
<h1>${data['title']}</h1>
% elif data['type'] == 'season':
<h1>${data['parent_title']} (${data['title']})</h1>
% elif data['type'] == 'episode':
<h1>${data['grandparent_title']} (Season ${data['parent_index']}, Episode
${data['index']}) "${data['title']}"</h1>
<h1>${data['grandparent_title']} - ${data['title']}
(Season ${data['parent_index']}, Episode ${data['index']})</h1>
% else:
<h1>${data['title']}</h1>
% endif
</div>
% if data['type'] == 'movie':
% if (data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode') and data['rating']:
<div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
data-rateit-ispreset="true" data-rateit-readonly="true"></div>
% endif
<div class="summary-content-details-wrapper">
<div class="summary-content-director">
% if data['type'] == 'episode' or data['type'] == 'movie':
% if data['directors']:
% if (data['type'] == 'episode' or data['type'] == 'movie') and data['directors']:
Directed by <strong> ${data['directors'][0]}</strong>
% else:
Directed by <strong> unknown</strong>
% endif
% elif data['type'] == 'show' or data['type'] == 'season':
</div>
<div class="summary-content-studio">
% if (data['type'] == 'show' or data['type'] == 'movie') and data['studio']:
Studio <strong> ${data['studio']}</strong>
% endif
</div>
<div class="summary-content-airdate">
% if data['type'] == 'movie':
Year <strong> ${data['year']}</strong>
% elif data['type'] == 'show':
Aired <strong> ${data['year']}</strong>
% elif data['type'] == 'episode':
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% endif
</div>
<div class="summary-content-duration">
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
</div>
<div class="summary-content-content-rating">
% if (data['type'] == 'episode' or data['type'] == 'movie' or data['type'] == 'show') and data['content_rating']:
Rated <strong> ${data['content_rating']} </strong>
% endif
</div>
</div>
<div class="summary-content-summary">
@@ -100,9 +113,23 @@ DOCUMENTATION :: END
</div>
</div>
</div>
% if data['type'] == 'episode':
<div class="col-md-3">
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
% if (data['type'] == 'movie' or data['type'] == 'show') and data['genres']:
<div class="summary-content-genres">
<strong>Genres</strong>
<ul>
% for genre in data['genres']:
% if loop.index < 5:
<li>
${genre}
</li>
% endif
% endfor
</ul>
</div>
% endif
% if (data['type'] == 'episode' or data['type'] == 'movie') and data['writers']:
<div class="summary-content-writers">
<strong>Written by</strong>
<ul>
@@ -115,42 +142,23 @@ DOCUMENTATION :: END
% endfor
</ul>
</div>
</div>
</div>
% elif data['type'] == 'movie' or data['type'] == 'show':
<div class="col-md-3">
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
% endif
% if (data['type'] == 'movie' or data['type'] == 'show') and data['actors']:
<div class="summary-content-actors">
<strong>Genres</strong>
<strong>Starring</strong>
<ul>
% for genre in data['genres']:
% for actor in data['actors']:
% if loop.index < 5:
<li>
${genre}
${actor}
</li>
% endif
% endfor
</ul>
</div>
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
<div class="summary-content-actors">
<strong>Starring</strong>
<ul>
% for actor in data['actors']:
% if loop.index < 5:
<li>
${actor}
</li>
% endif
% endfor
</ul>
</div>
</div>
% endif
</div>
</div>
% elif data['type'] == 'season':
<div class="col-md-3"></div>
% endif
</div>
</div>
</div>
@@ -164,25 +172,28 @@ DOCUMENTATION :: END
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Watch history for <strong>${data['title']}</strong></span>
<span>Watch History for <strong>${data['title']}</strong></span>
</div>
<div class="colvis-button-bar hidden-xs">
<div class="button-bar">
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete mode</button>&nbsp
<div class="colvis-button-bar hidden-xs"></div>
</div>
</div>
<div class="table-card-back">
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody>
@@ -202,7 +213,7 @@ DOCUMENTATION :: END
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Episode list for <strong>${data['title']}</strong></span>
<span>Episode List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
@@ -233,7 +244,7 @@ DOCUMENTATION :: END
<script src="interfaces/default/js/moment-with-locale.js"></script>
% if data:
% if data['type'] == 'movie':
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode':
<script>
// Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2)
@@ -254,10 +265,23 @@ DOCUMENTATION :: END
}
}
history_table = $('#history_table').DataTable(history_table_options);
history_table.column(4).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table);
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
});
</script>
% elif data['type'] == 'show':
@@ -274,8 +298,22 @@ DOCUMENTATION :: END
}
}
history_table = $('#history_table').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table);
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
});
</script>
% endif
@@ -294,6 +332,7 @@ DOCUMENTATION :: END
</script>
% endif
<script>
$("#airdate").html(moment($("#airdate")).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").html(), true));
</script>
% endif

View File

@@ -30,25 +30,20 @@ DOCUMENTATION :: END
<ul class="season-episodes-instance list-unstyled">
% for a in data['episode_list']:
<li>
<div class="season-episodes-poster">
<div class="season-episodes-poster-face">
<a href="info?item_id=${a['rating_key']}">
<img src="pms_image_proxy?img=${a['thumb']}&width=410&height=230" class="season-episodes-poster-face">
</a>
</div>
<div class="season-episodes-card-overlay">
<div class="season-episodes-season">
Episode ${a['index']}
<a href="info?item_id=${a['rating_key']}">
<div class="season-episodes-poster">
<div class="season-episodes-poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=410&height=230);">
<div class="season-episodes-card-overlay">
<div class="season-episodes-season">
Episode ${a['index']}
</div>
</div>
</div>
</div>
</div>
<div class="season-episodes-instance-text-wrapper">
<div class="season-episodes-title">
<a href="info?item_id=${a['rating_key']}">
"${a['title']}"
</a>
<div class="season-episodes-instance-text-wrapper">
<h3>${a['title']}</h3>
</div>
</div>
</a>
</li>
% endfor
</ul>

View File

@@ -15,7 +15,7 @@
<div class="modal-body" id="modal-text">
<div class="col-md-6">
<h4><strong>Location Details</strong></h4>
<ul>
<ul class="list-unstyled">
<li>Country: <strong><span id="country"></span></strong></li>
<li>City: <strong><span id="city"></span></strong></li>
<li>Region: <strong><span id="region"></span></strong></li>
@@ -26,7 +26,7 @@
</div>
<div class="col-md-6">
<h4><strong>Connection Details</strong></h4>
<ul>
<ul class="list-unstyled">
<li>ISP: <strong><span id="isp"></span></strong></li>
<li>Organization: <strong><span id="org"></span></strong></li>
<li>AS: <strong><span id="as"></span></strong></li>

View File

@@ -251,6 +251,9 @@ function humanTime(seconds) {
} else if (seconds >= 60) {
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
return text;
} else {
text = '<h3>0</h3><p> mins</p>';
return text;
}
}
@@ -348,3 +351,12 @@ Accordion.prototype.dropdown = function(e) {
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
};
}
function clearSearchButton(tableName, table) {
$('#' + tableName + '_filter').find('input[type=search]')
.wrap('<div class="input-group" role="group" aria-label="Search"></div>')
.after('<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' + tableName + '"><i class="fa fa-remove"></i></button></span>')
$('#clear-search-' + tableName).click(function() {
table.search('').draw();
});
}

View File

@@ -25,10 +25,22 @@ history_table_options = {
"processing": false,
"serverSide": true,
"pageLength": 25,
"order": [ 0, 'desc'],
"order": [ 1, 'desc'],
"autoWidth": false,
"columnDefs": [
{
"targets": [0],
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<button class="btn btn-xs btn-danger" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o"></i> Delete</button>');
},
"width": "5%",
"className": "delete-control no-wrap hidden",
"searchable": false,
"orderable": false
},
{
"targets": [1],
"data":"date",
"createdCell": function (td, cellData, rowData, row, col) {
if (rowData['stopped'] === null) {
@@ -38,10 +50,11 @@ history_table_options = {
}
},
"searchable": false,
"width": "8%",
"className": "no-wrap"
},
{
"targets": [1],
"targets": [2],
"data":"friendly_name",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -54,21 +67,12 @@ history_table_options = {
$(td).html(cellData);
}
},
"width": "8%",
"className": "no-wrap hidden-xs"
},
{
"targets": [2],
"data":"player",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html('<a href="#" data-target="#info-modal" data-toggle="modal"><i class="fa fa-lg fa-info-circle"></i></a>&nbsp'+cellData);
}
},
"className": "modal-control no-wrap hidden-sm hidden-xs"
},
{
"targets": [3],
"data":"ip_address",
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
if (isPrivateIP(cellData)) {
@@ -78,35 +82,63 @@ history_table_options = {
$(td).html('n/a');
}
} else {
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><i class="fa fa-map-marker"></i>&nbsp' + cellData +'</a>');
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><i class="fa fa-map-marker"></i>&nbsp' + cellData + '</a>');
}
} else {
$(td).html('n/a');
}
},
"className": "no-wrap hidden-xs modal-control-ip"
"width": "8%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
},
{
"targets": [4],
"data":"player",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>&nbsp';
} else if (rowData['video_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>&nbsp';
} else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>&nbsp';
}
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp' + cellData + '</div></a></div>');
}
},
"width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [5],
"data":"full_title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<i class="fa fa-server"></i>&nbsp';
}
$(td).html('<div><div style="float: left;"><a href="info?source=history&item_id=' + rowData['id'] + '">' + cellData + '</a></div><div style="float: right; text-align: right; padding-right: 5px;">' + transcode_dec + '<i class="fa fa-video-camera"></i></div></div>');
var media_type = '';
var thumb_popover = '';
if (rowData['media_type'] === 'movie') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + ' (' + rowData['year'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + ' \
(S' + ('00' + rowData['parent_media_index']).slice(-2) + 'E' + ('00' + rowData['media_index']).slice(-2) + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
$(td).html('<div><div style="float: left;">' + cellData + '</div><div style="float: right; text-align: right; padding-right: 5px;"><i class="fa fa-music"></i></div></div>');
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
} else {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
}
}
}
},
"width": "35%"
},
{
"targets": [5],
"targets": [6],
"data":"started",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) {
@@ -116,10 +148,11 @@ history_table_options = {
}
},
"searchable": false,
"width": "5%",
"className": "no-wrap hidden-sm hidden-xs"
},
{
"targets": [6],
"targets": [7],
"data":"paused_counter",
"render": function ( data, type, full ) {
if (data !== null) {
@@ -129,10 +162,11 @@ history_table_options = {
}
},
"searchable": false,
"className": "no-wrap hidden-xs"
"width": "5%",
"className": "no-wrap hidden-md hidden-sm hidden-xs"
},
{
"targets": [7],
"targets": [8],
"data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) {
@@ -142,10 +176,11 @@ history_table_options = {
}
},
"searchable": false,
"className": "no-wrap hidden-md hidden-xs"
"width": "5%",
"className": "no-wrap hidden-sm hidden-xs"
},
{
"targets": [8],
"targets": [9],
"data":"duration",
"render": function ( data, type, full ) {
if (data !== null) {
@@ -155,34 +190,50 @@ history_table_options = {
}
},
"searchable": false,
"width": "5%",
"className": "no-wrap hidden-xs"
},
{
"targets": [9],
"targets": [10],
"data":"percent_complete",
"render": function ( data, type, full ) {
if (data > 80) {
return '<i class="fa fa-lg fa-circle"></i>'
return '<span class="watched-tooltip" data-toggle="tooltip" title="Watched"><i class="fa fa-lg fa-circle"></i></span>'
} else if (data > 40) {
return '<i class="fa fa-lg fa-adjust fa-rotate-180"></i>'
return '<span class="watched-tooltip" data-toggle="tooltip" title="Partial"><i class="fa fa-lg fa-adjust fa-rotate-180"></i></span>'
} else {
return '<i class="fa fa-lg fa-circle-o"></i>'
return '<span class="watched-tooltip" data-toggle="tooltip" title="Unwatched"><i class="fa fa-lg fa-circle-o"></i></span>'
}
},
"searchable": false,
"orderable": false,
"className": "no-wrap hidden-md hidden-xs",
"width": "10px"
}
"className": "no-wrap hidden-md hidden-sm hidden-xs",
"width": "1%"
},
],
"drawCallback": function (settings) {
// Jump to top of page
// $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.info-modal').each(function() {
$(this).tooltip();
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
$('.thumb-tooltip').popover({
html: true,
trigger: 'hover',
placement: 'right',
content: function () {
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
}
});
if ($('#row-edit-mode').hasClass('active')) {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
},
"preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
@@ -228,6 +279,24 @@ $('#history_table').on('click', 'td.modal-control-ip', function () {
});
}
}
getUserLocation(rowData['ip_address']);
});
$('#history_table').on('click', 'td.delete-control > button', function () {
var tr = $(this).parents('tr');
var row = history_table.row( tr );
var rowData = row.data();
$(this).prop('disabled', true);
$(this).html('<i class="fa fa-spin fa-refresh"></i> Delete');
$.ajax({
url: 'delete_history_rows',
data: {row_id: rowData['id']},
async: true,
success: function(data) {
history_table.ajax.reload(null, false);
}
});
});

View File

@@ -74,7 +74,7 @@ history_table_modal_options = {
{
"targets": [3],
"data":"player",
"className": "modal-control no-wrap hidden-sm hidden-xs"
"className": "no-wrap hidden-sm hidden-xs modal-control"
},
{
"targets": [4],

View File

@@ -24,13 +24,11 @@ user_ip_table_options = {
},
"searchable": false,
"width": "15%",
"className": "no-wrap"
"className": "no-wrap hidden-xs"
},
{
"targets": [1],
"data":"ip_address",
"width": "15%",
"className": "modal-control no-wrap",
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
if (isPrivateIP(cellData)) {
@@ -46,31 +44,83 @@ user_ip_table_options = {
$(td).html('n/a');
}
},
"width": "15%"
"width": "15%",
"className": "no-wrap modal-control-ip"
},
{
"targets": [2],
"data":"play_count",
"width": "10%",
"className": "hidden-xs"
"data":"platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>&nbsp';
} else if (rowData['video_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>&nbsp';
} else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>&nbsp';
}
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp' + cellData + '</div></a></div>');
} else {
$(td).html('n/a');
}
},
"width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [3],
"data":"platform",
"width": "15%",
"className": "hidden-xs"
"data":"last_watched",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var media_type = '';
var thumb_popover = ''
if (rowData['media_type'] === 'movie') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
} else if (rowData['media_type']) {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
} else {
$(td).html('n/a');
}
}
},
"className": "hidden-sm hidden-xs"
},
{
"targets": [4],
"data":"last_watched",
"width": "30%",
"className": "hidden-sm hidden-xs"
}
"data":"play_count",
"searchable": false,
"width": "10%"
}
],
"drawCallback": function (settings) {
// Jump to top of page
// $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
$('.thumb-tooltip').popover({
html: true,
trigger: 'hover',
placement: 'right',
content: function () {
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
}
});
},
"preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
@@ -83,6 +133,25 @@ $('#user_ip_table').on('mouseenter', 'td.modal-control span', function () {
});
$('#user_ip_table').on('click', 'td.modal-control', function () {
var tr = $(this).parents('tr');
var row = user_ip_table.row(tr);
var rowData = row.data();
function showStreamDetails() {
$.ajax({
url: 'get_stream_data',
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
cache: false,
async: true,
complete: function (xhr, status) {
$("#info-modal").html(xhr.responseText);
}
});
}
showStreamDetails();
});
$('#user_ip_table').on('click', 'td.modal-control-ip', function () {
var tr = $(this).parents('tr');
var row = user_ip_table.row( tr );
var rowData = row.data();

View File

@@ -18,7 +18,7 @@ users_list_table_options = {
"columnDefs": [
{
"targets": [0],
"data": "thumb",
"data": "user_thumb",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === '') {
$(td).html('<img src="interfaces/default/images/gravatar-default-80x80.png" alt="User Logo"/>');
@@ -28,13 +28,13 @@ users_list_table_options = {
},
"orderable": false,
"searchable": false,
"className": "users-poster-face",
"width": "40px"
"width": "5%",
"className": "users-poster-face"
},
{
"targets": [1],
"data": "friendly_name",
"createdCell": function (td, cellData, rowData, row, col) {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id'] > 0) {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
@@ -45,6 +45,7 @@ users_list_table_options = {
$(td).html(cellData);
}
},
"width": "15%"
},
{
"targets": [2],
@@ -57,34 +58,150 @@ users_list_table_options = {
}
},
"searchable": false,
"className": "hidden-xs",
"width": "15%",
"className": "no-wrap hidden-xs"
},
{
"targets": [3],
"data": "ip_address",
"render": function ( data, type, full ) {
if (data) {
return data;
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
if (isPrivateIP(cellData)) {
if (cellData != '') {
$(td).html(cellData);
} else {
$(td).html('n/a');
}
} else {
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><span data-toggle="ip-tooltip" data-placement="left" title="IP Address Info" id="ip-info"><i class="fa fa-map-marker"></i></span>&nbsp' + cellData + '</a>');
}
} else {
return "n/a";
$(td).html('n/a');
}
},
"className": "hidden-xs",
"width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
},
{
"targets": [4],
"data":"platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>&nbsp';
} else if (rowData['video_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>&nbsp';
} else if (rowData['video_decision'] === 'direct play' || rowData['video_decision'] === '') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>&nbsp';
}
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp' + cellData + '</div></a></div>');
} else {
$(td).html('n/a');
}
},
"width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [5],
"data":"last_watched",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var media_type = '';
var thumb_popover = ''
if (rowData['media_type'] === 'movie') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
} else if (rowData['media_type']) {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
} else {
$(td).html('n/a');
}
}
},
"className": "hidden-sm hidden-xs"
},
{
"targets": [6],
"data": "plays",
"searchable": false
}
"searchable": false,
"width": "10%"
}
],
"drawCallback": function (settings) {
// Jump to top of page
//$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
$('.thumb-tooltip').popover({
html: true,
trigger: 'hover',
placement: 'right',
content: function () {
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
}
});
},
"preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
showMsg(msg, false, false, 0)
}
}
$('#users_list_table').on('click', 'td.modal-control', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
function showStreamDetails() {
$.ajax({
url: 'get_stream_data',
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
cache: false,
async: true,
complete: function (xhr, status) {
$("#info-modal").html(xhr.responseText);
}
});
}
showStreamDetails();
});
$('#users_list_table').on('click', 'td.modal-control-ip', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
function getUserLocation(ip_address) {
if (isPrivateIP(ip_address)) {
return "n/a"
} else {
$.ajax({
url: 'get_ip_address_details',
data: { ip_address: ip_address },
async: true,
complete: function (xhr, status) {
$("#ip-info-modal").html(xhr.responseText);
}
});
}
}
getUserLocation(rowData['ip_address']);
});

View File

@@ -86,8 +86,9 @@ from plexpy import helpers
$(document).ready(function() {
LoadPlexPyLogs();
clearSearchButton('log_table', log_table);
});
function LoadPlexPyLogs() {
log_table_options.ajax = {
"url": "getLog"
@@ -105,11 +106,13 @@ from plexpy import helpers
$("#plexpy-logs-btn").click(function() {
$("#clear-logs").show();
LoadPlexPyLogs();
clearSearchButton('log_table', log_table);
});
$("#plex-logs-btn").click(function() {
$("#clear-logs").hide();
LoadPlexLogs();
clearSearchButton('plex_log_table', plex_log_table);
});
$("#clear-logs").click(function() {

View File

@@ -29,29 +29,32 @@ DOCUMENTATION :: END
% for item in data:
<div class="dashboard-recent-media-instance">
<li>
<div class="poster">
% if item['type'] == 'season' or item['type'] == 'movie':
<div class="poster-face">
<a href="info?item_id=${item['rating_key']}">
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
</a>
% if item['type'] == 'season' or item['type'] == 'movie':
<a href="info?item_id=${item['rating_key']}">
<div class="poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% elif item['type'] == 'album':
<div class="cover-face">
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover" class="cover-face">
</div>
% endif
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season':
<h3>${item['parent_title']} - ${item['title']}</h3>
% elif item['type'] == 'album':
<h3>${item['title']}</h3>
% elif item['type'] == 'movie':
<h3>${item['title']} (${item['year']})</h3>
% endif
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season':
<h3>${item['parent_title']}</h3>
<h3>(${item['title']})</h3>
% elif item['type'] == 'movie':
<h3>${item['title']}</h3>
<h3>(${item['year']})</h3>
% endif
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
</a>
% elif item['type'] == 'album':
<div class="poster">
<div class="cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"></div>
</div>
<div class="dashboard-recent-media-metacontainer">
<h3>${item['parent_title']}</h3>
<h3>${item['title']}</h3>
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
% endif
</li>
</div>
<script>

View File

@@ -1,7 +1,7 @@
<%inherit file="base.html"/>
<%!
import plexpy
from plexpy import notifiers
from plexpy import notifiers, common, versioncheck
available_notification_agents = notifiers.available_notification_agents()
%>
@@ -48,9 +48,11 @@ available_notification_agents = notifiers.available_notification_agents()
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-1">
% if common.VERSION_NUMBER:
<div class="padded-header">
<h3>Software Updates</h3>
<h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
</div>
% endif
<div class="checkbox">
<label>
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
@@ -58,7 +60,7 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
</div>
% if plexpy.CURRENT_VERSION:
<p>Current version: ${plexpy.CURRENT_VERSION}</p>
<p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
% endif
<div class="padded-header">
<h3>Display Settings</h3>
@@ -276,8 +278,12 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">If you have media indexing enabled on your server, use these on the activity pane.</p>
</div>
<div class="padded-header">
<h3>Homepage Statistics</h3>
</div>
<div class="form-group">
<label for="home_stats_length">Homepage Statistics Time Frame</label>
<label for="home_stats_length">Time Frame</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="home_stats_length" name="home_stats_length" value="${config['home_stats_length']}" size="3" data-parsley-min="0" data-parsley-trigger="change" required>
@@ -285,6 +291,12 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<p class="help-block">Specify the number of days for the statistics on the home page. Default is 30 days.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="home_stats_type" name="home_stats_type" value="1" ${config['home_stats_type']}> Use play duration
</label>
<p class="help-block">Use play duration instead of play count to generate statistics.</p>
</div>
<div class="padded-header">
<h3>Plex Logs</h3>
@@ -810,10 +822,18 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{season_num}</strong></td>
<td>The season number for the media item if item is episode.</td>
</tr>
<tr>
<td width="150"><strong>{season_num00}</strong></td>
<td>The two digit season number.</td>
</tr>
<tr>
<td width="150"><strong>{episode_num}</strong></td>
<td>The episode number for the media item if item is episode.</td>
</tr>
<tr>
<td width="150"><strong>{episode_num00}</strong></td>
<td>The two digit episode number.</td>
</tr>
<tr>
<td width="150"><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</td>
@@ -822,6 +842,10 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{duration}</strong></td>
<td>The duration (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{stream_duration}</strong></td>
<td>The stream duration (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{progress}</strong></td>
<td>The last reported offset (in minutes) for the item.</td>
@@ -875,6 +899,21 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
</div>
</div>
<div id="changelog-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Changelog</h4>
</div>
<div class="modal-body">
${versioncheck.read_changelog()}
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</div>
</%def>
@@ -1023,7 +1062,9 @@ $(document).ready(function() {
headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': 'PlexPy',
'X-Plex-Version': 'v0.1 dev',
'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
},

View File

@@ -55,7 +55,7 @@ DOCUMENTATION :: END
<div class="col-md-4">
<h4><strong>Stream Details</strong></h4>
<h5>Video</h5>
<ul>
<ul class="list-unstyled">
% if data['transcode_video_dec'] != 'direct play':
<li>Stream Type: <strong>${data['transcode_video_dec']}</strong></li>
<li>Video Resolution: <strong>${data['transcode_height']}p</strong></li>
@@ -75,7 +75,7 @@ DOCUMENTATION :: END
% endif
</ul>
<h5>Audio</h5>
<ul>
<ul class="list-unstyled">
% if data['transcode_audio_dec'] != 'direct play':
<li>Stream Type: <strong>${data['transcode_audio_dec']}</strong></li>
<li>Audio Codec: <strong>${data['transcode_audio_codec']}</strong></li>
@@ -89,7 +89,7 @@ DOCUMENTATION :: END
</div>
<div class="col-md-4">
<h4><strong>Media Source Details</strong></h4>
<ul>
<ul class="list-unstyled">
<li>Container: <strong>${data['container']}</strong></li>
<li>Resolution: <strong>${data['height']}p</strong></li>
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
@@ -97,7 +97,7 @@ DOCUMENTATION :: END
</div>
<div class="col-md-4">
<h4><strong>Video Source Details</strong></h4>
<ul>
<ul class="list-unstyled">
<li>Width: <strong>${data['width']}</strong></li>
<li>Height: <strong>${data['height']}</strong></li>
<li>Aspect Ratio: <strong>${data['aspect_ratio']}</strong></li>
@@ -105,7 +105,7 @@ DOCUMENTATION :: END
<li>Video Codec: <strong>${data['video_codec']}</strong></li>
</ul>
<h4><strong>Audio Source Details</strong></h4>
<ul>
<ul class="list-unstyled">
<li>Audio Codec: <strong>${data['audio_codec']}</strong></li>
<li>Audio Channels: <strong>${data['audio_channels']}</strong></li>
</ul>

View File

@@ -57,9 +57,10 @@
"url": "get_sync"
}
sync_table = $('#sync_table').DataTable(sync_table_options);
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis.button() ).appendTo('div.colvis-button-bar');
clearSearchButton('sync_table', sync_table);
});
</script>
</%def>

View File

@@ -103,7 +103,7 @@ from plexpy import helpers
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span><i class="fa fa-history"></i> Recently watched</span>
<span><i class="fa fa-history"></i> Recently Watched</span>
</div>
</div>
<div class="table-card-back">
@@ -131,17 +131,15 @@ from plexpy import helpers
<table id="user_ip_table" class="display" width="100%">
<thead>
<tr>
<th align="left">Last seen</th>
<th align="left">Last Seen</th>
<th align="left">IP Address</th>
<th align="left">Play Count</th>
<th align="left">Platform (Last Seen)</th>
<th align="left">Last Platform</th>
<th align="left">Last Watched</th>
<th align="left">Play Count</th>
</tr>
</thead>
</table>
</div>
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div>
</div>
</div>
@@ -156,13 +154,17 @@ from plexpy import helpers
<span class="set-username">${data['friendly_name']}</span>
</strong></span>
</div>
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
<div class="button-bar">
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete Mode</button>&nbsp
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
</div>
</div>
</div>
<div class="table-card-back">
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th>
@@ -172,14 +174,13 @@ from plexpy import helpers
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal"></div>
</div>
</div>
</div>
@@ -222,6 +223,10 @@ from plexpy import helpers
</div>
</div>
</div>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div>
<footer></footer>
</%def>
@@ -236,6 +241,33 @@ from plexpy import helpers
<script src="interfaces/default/js/tables/user_ips.js"></script>
<script src="interfaces/default/js/tables/sync_table.js"></script>
<script>
function recentlyWatched() {
var widthVal = $('body').find("#user-recently-watched").width();
var tmp = (widthVal-32) / 180;
if (tmp > 0) {
containerSize = parseInt(tmp);
} else {
containerSize = 1;
}
% if data['user_id']:
var user_id = ${data['user_id']};
% else:
var user_id = null;
% endif
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: { user_id: user_id, user: '${data['username']}', limit: containerSize },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
}
$(document).ready(function () {
% if data['user_id']:
@@ -266,16 +298,6 @@ from plexpy import helpers
}
});
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: { user_id: user_id, user: '${data['username']}' },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
$( "#history-tab-btn" ).one( "click", function() {
// Build watch history table
history_table_options.ajax = {
@@ -289,10 +311,12 @@ from plexpy import helpers
}
}
history_table = $('#history_table').DataTable(history_table_options);
history_table.column(1).visible(false);
history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table);
});
$( "#ip-tab-btn" ).one( "click", function() {
@@ -308,6 +332,8 @@ from plexpy import helpers
}
}
user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
clearSearchButton('user_ip_table', user_ip_table);
});
$( "#sync-tab-btn" ).one( "click", function() {
@@ -320,10 +346,12 @@ from plexpy import helpers
}
}
sync_table = $('#sync_table').DataTable(sync_table_options);
history_table.column(1).visible(false);
sync_table.column(1).visible(false);
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis_sync.button() ).appendTo('#button-bar-sync');
clearSearchButton('sync_table', sync_table);
});
// Load edit user modal
@@ -339,6 +367,24 @@ from plexpy import helpers
}
});
});
// Delete mode button
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
recentlyWatched();
$(window).resize(function() {
recentlyWatched();
});
});
</script>
</%def>

View File

@@ -34,7 +34,7 @@ DOCUMENTATION :: END
</div>
</ul>
<script>
$("#user-platform-image-${a['result_id']}").html("<img class='user-platforms-instance-poster' src='" + getPlatformImagePath('${a['platform_type']}') + "'>");
$("#user-platform-image-${a['result_id']}").html("<div class='user-platforms-instance-poster' style='background-image: url(" + getPlatformImagePath('${a['platform_type']}') + ");'>");
</script>
% endfor
% else:

View File

@@ -18,6 +18,7 @@ time Returns the last watched time of the media.
title Returns the name of the movie or episode.
== Only if 'type' is 'episode ==
parent_title Returns the name of the TV Show a season belongs too.
parent_index Returns the season number.
index Returns the episode number.
@@ -33,21 +34,22 @@ DOCUMENTATION :: END
% for item in data:
<div class="dashboard-recent-media-instance">
<li>
<div class="poster">
<div class="poster-face">
<a href="info?source=history&item_id=${item['row_id']}">
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
</a>
<a href="info?source=history&item_id=${item['row_id']}">
<div class="poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'episode':
<h3>Season ${item['parentIndex']}, Episode ${item['index']}</h3>
% elif item['type'] == 'movie':
<h3>${item['title']} (${item['year']})</h3>
% endif
<div class="text-muted" id="time-${item['time']}">${item['time']}</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'episode':
<h3>${item['parent_title']}</h3>
<h3>${item['title']}</h3>
<h3>(Season ${item['parent_index']}, Episode ${item['index']})</h3>
% elif item['type'] == 'movie':
<h3>${item['title']}</h3>
<h3>(${item['year']})</h3>
% endif
<div class="text-muted" id="time-${item['time']}">${item['time']}</div>
</div>
</a>
</li>
</div>
<script>

View File

@@ -30,8 +30,8 @@ DOCUMENTATION :: END
<h4>Last ${a['query_days']} days</h4>
% endif
<h3>${a['total_plays']}</h3>
<p>plays</p>
<h3><strong>/</strong></h3>
<span id="total-time-${a['query_days']}"></span>
</div>
</li>

View File

@@ -23,12 +23,18 @@
<th align="left" id="friendly_name">User</th>
<th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_known_ip">Last Known IP</th>
<th align="left" id="last_platform">Last Platform</th>
<th align="left" id="last_watched">Last Watched</th>
<th align="left" id="total_plays">Total Plays</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div>
</div>
@@ -41,15 +47,20 @@
<script src="interfaces/default/js/moment-with-locale.js"></script>
<script src="interfaces/default/js/tables/users.js"></script>
<script>
users_list_table_options.ajax = {
"url": "get_user_list",
type: "post",
data: function ( d ) {
return { 'json_data': JSON.stringify( d ) };
$(document).ready(function() {
users_list_table_options.ajax = {
"url": "get_user_list",
type: "post",
data: function ( d ) {
return { 'json_data': JSON.stringify( d ) };
}
}
}
var users_list_table = $('#users_list_table').DataTable(users_list_table_options);
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
clearSearchButton('users_list_table', users_list_table);
});
$("#refresh-users-list").click(function() {
$.ajax({

View File

@@ -1,6 +1,6 @@
<%
import plexpy
from plexpy import version
from plexpy import common
%>
<!doctype html>
@@ -355,7 +355,9 @@ from plexpy import version
headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': 'PlexPy',
'X-Plex-Version': 'v0.1 dev',
'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
},

View File

@@ -1,4 +1,4 @@
# PlexPy - Automatic music downloader for SABnzbd
# PlexPy
#
# Service Unit file for systemd system manager
#
@@ -53,7 +53,7 @@
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit]
Description=PlexPy - Automatic music downloader for SABnzbd
Description=PlexPy
[Service]
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet

80
init-scripts/init.freenas Normal file
View File

@@ -0,0 +1,80 @@
#!/bin/sh
#
# PROVIDE: plexpy
# REQUIRE: DAEMON sabnzbd
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# plexpy_enable (bool): Set to NO by default.
# Set it to YES to enable it.
# plexpy_user: The user account PlexPy daemon runs as what
# you want it to be. It uses '_sabnzbd' user by
# default. Do not sets it as empty or it will run
# as root.
# plexpy_dir: Directory where PlexPy lives.
# Default: /usr/local/plexpy
# plexpy_chdir: Change to this directory before running PlexPy.
# Default is same as plexpy_dir.
# plexpy_pid: The name of the pidfile to create.
# Default is plexpy.pid in plexpy_dir.
PATH="/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin"
. /etc/rc.subr
name="plexpy"
rcvar=${name}_enable
load_rc_config ${name}
: ${plexpy_enable:="NO"}
: ${plexpy_user:="_sabnzbd"}
: ${plexpy_dir:="/usr/local/share/plexpy"}
: ${plexpy_chdir:="${plexpy_dir}"}
: ${plexpy_pid:="${plexpy_dir}/plexpy.pid"}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="/usr/sbin/daemon"
command_args="-f -p ${plexpy_pid} python2 ${plexpy_dir}/PlexPy.py ${plexpy_flags} --quiet --nolaunch"
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
echo "Oops, you should be root before running this!"
exit 1
fi
verify_plexpy_pid() {
# Make sure the pid corresponds to the PlexPy process.
if [ -f ${plexpy_pid} ]; then
pid=`cat ${plexpy_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${plexpy_dir}/PlexPy.py"
return $?
else
return 0
fi
}
# Try to stop PlexPy cleanly by calling shutdown over http.
plexpy_stop() {
echo "Stopping $name."
verify_plexpy_pid
if [ -n "${pid}" ]; then
kill ${pid}
wait_for_pids ${pid}
echo "Stopped."
fi
}
plexpy_status() {
verify_plexpy_pid
if [ -n "${pid}" ]; then
echo "$name is running as ${pid}."
else
echo "$name is not running."
fi
}
run_rc_command "$1"

View File

@@ -1,4 +1,4 @@
# plexpy - Automatic music downloader
# plexpy
#
# This is a session/user job. Install this file into /usr/share/upstart/sessions
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if

View File

@@ -19,14 +19,17 @@ Created on Aug 1, 2011
@author: Michael
'''
import platform
import operator
import os
import re
from plexpy import version
# Identify Our Application
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
' ' + platform.release() + ')'
PLATFORM = platform.system()
PLATFORM_VERSION = platform.release()
BRANCH = version.PLEXPY_VERSION
VERSION_NUMBER = version.PLEXPY_RELEASE_VERSION
# Notification Types
NOTIFY_STARTED = 1

View File

@@ -83,6 +83,7 @@ _CONFIG_DEFINITIONS = {
'GROWL_ON_BUFFER': (int, 'Growl', 0),
'GROWL_ON_WATCHED': (int, 'Growl', 0),
'HOME_STATS_LENGTH': (int, 'General', 30),
'HOME_STATS_TYPE': (int, 'General', 0),
'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'),

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# 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
@@ -36,6 +36,13 @@ class DataFactory(object):
'session_history.player',
'session_history.ip_address',
'session_history_metadata.full_title as full_title',
'session_history_metadata.thumb',
'session_history_metadata.parent_thumb',
'session_history_metadata.grandparent_thumb',
'session_history_metadata.media_index',
'session_history_metadata.parent_media_index',
'session_history_metadata.parent_title',
'session_history_metadata.year',
'session_history.started',
'session_history.paused_counter',
'session_history.stopped',
@@ -81,12 +88,24 @@ class DataFactory(object):
rows = []
for item in history:
if item["media_type"] == 'episode' and item["parent_thumb"]:
thumb = item["parent_thumb"]
elif item["media_type"] == 'episode':
thumb = item["grandparent_thumb"]
else:
thumb = item["thumb"]
row = {"id": item['id'],
"date": item['date'],
"friendly_name": item['friendly_name'],
"player": item["player"],
"ip_address": item["ip_address"],
"full_title": item["full_title"],
"thumb": thumb,
"media_index": item["media_index"],
"parent_media_index": item["parent_media_index"],
"parent_title": item["parent_title"],
"year": item["year"],
"started": item["started"],
"paused_counter": item["paused_counter"],
"stopped": item["stopped"],
@@ -110,14 +129,16 @@ class DataFactory(object):
return dict
def get_home_stats(self, time_range='30'):
def get_home_stats(self, time_range='30', stat_type='0'):
monitor_db = database.MonitorDatabase()
if not time_range.isdigit():
time_range = '30'
sort_type = 'total_plays' if stat_type == '0' else 'total_duration'
# This actually determines the output order in the home page
stats_queries = ["top_tv", "popular_tv", "top_movies", "top_users", "top_platforms"]
stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms"]
home_stats = []
for stat in stats_queries:
@@ -127,6 +148,10 @@ class DataFactory(object):
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.grandparent_title, ' \
'COUNT(session_history_metadata.grandparent_title) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch,' \
'session_history_metadata.grandparent_thumb ' \
@@ -136,7 +161,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 total_plays DESC LIMIT 10' % time_range
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -145,10 +170,11 @@ class DataFactory(object):
for item in result:
row = {'title': item[1],
'total_plays': item[2],
'total_duration': item[3],
'users_watched': '',
'rating_key': item[3],
'last_play': item[4],
'grandparent_thumb': item[5],
'rating_key': item[4],
'last_play': item[5],
'grandparent_thumb': item[6],
'thumb': '',
'user': '',
'friendly_name': '',
@@ -159,6 +185,7 @@ class DataFactory(object):
top_tv.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_tv})
elif 'top_movies' in stat:
@@ -167,6 +194,10 @@ class DataFactory(object):
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \
'COUNT(session_history_metadata.full_title) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch,' \
'session_history_metadata.thumb ' \
@@ -176,7 +207,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 total_plays DESC LIMIT 10' % time_range
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -185,11 +216,12 @@ class DataFactory(object):
for item in result:
row = {'title': item[1],
'total_plays': item[2],
'total_duration': item[3],
'users_watched': '',
'rating_key': item[3],
'last_play': item[4],
'rating_key': item[4],
'last_play': item[5],
'grandparent_thumb': '',
'thumb': item[5],
'thumb': item[6],
'user': '',
'friendly_name': '',
'platform_type': '',
@@ -199,6 +231,7 @@ class DataFactory(object):
top_movies.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_movies})
elif 'popular_tv' in stat:
@@ -243,6 +276,48 @@ class DataFactory(object):
home_stats.append({'stat_id': stat,
'rows': popular_tv})
elif 'popular_movies' in stat:
popular_movies = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \
'COUNT(DISTINCT session_history.user_id) as users_watched, ' \
'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch, ' \
'COUNT(session_history.id) as total_plays, ' \
'session_history_metadata.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 = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \
'ORDER BY users_watched DESC, total_plays DESC ' \
'LIMIT 10' % time_range
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': '',
'thumb': item[6],
'user': '',
'friendly_name': '',
'platform_type': '',
'platform': '',
'row_id': item[0]
}
popular_movies.append(row)
home_stats.append({'stat_id': stat,
'rows': popular_movies})
elif 'top_users' in stat:
top_users = []
try:
@@ -250,6 +325,10 @@ class DataFactory(object):
'(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \
'COUNT(session_history.id) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'MAX(session_history.started) as last_watch, ' \
'users.custom_avatar_url as thumb, ' \
'users.user_id ' \
@@ -259,23 +338,24 @@ class DataFactory(object):
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") '\
'GROUP BY session_history.user_id ' \
'ORDER BY total_plays DESC LIMIT 10' % time_range
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
if not item[4] or item[4] == '':
if not item[5] or item[5] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item[4]
user_thumb = item[5]
row = {'user': item[0],
'user_id': item[5],
'user_id': item[6],
'friendly_name': item[1],
'total_plays': item[2],
'last_play': item[3],
'total_duration': item[3],
'last_play': item[4],
'thumb': user_thumb,
'grandparent_thumb': '',
'users_watched': '',
@@ -288,6 +368,7 @@ class DataFactory(object):
top_users.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_users})
elif 'top_platforms' in stat:
@@ -296,6 +377,10 @@ class DataFactory(object):
try:
query = 'SELECT session_history.platform, ' \
'COUNT(session_history.id) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'MAX(session_history.started) as last_watch ' \
'FROM session_history ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
@@ -310,7 +395,8 @@ class DataFactory(object):
for item in result:
row = {'platform': item[0],
'total_plays': item[1],
'last_play': item[2],
'total_duration': item[2],
'last_play': item[3],
'platform_type': item[0],
'title': '',
'thumb': '',
@@ -324,6 +410,7 @@ class DataFactory(object):
top_platform.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_platform})
return home_stats
@@ -343,7 +430,6 @@ class DataFactory(object):
else:
return None
print result
stream_output = {}
for item in result:
@@ -381,21 +467,21 @@ class DataFactory(object):
try:
if user_id:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user_id = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user_id, limit])
elif user:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user, limit])
else:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata WHERE session_history.media_type != "track"' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'ORDER BY started DESC LIMIT ?'
@@ -405,21 +491,24 @@ class DataFactory(object):
return None
for row in result:
if row[1] == 'episode':
thumb = row[5]
if row[1] == 'episode' and row[6]:
thumb = row[6]
elif row[1] == 'episode':
thumb = row[7]
else:
thumb = row[4]
thumb = row[5]
recent_output = {'row_id': row[0],
'type': row[1],
'rating_key': row[2],
'title': row[3],
'parent_title': row[4],
'thumb': thumb,
'index': row[6],
'parentIndex': row[7],
'year': row[8],
'time': row[9],
'user': row[10]
'index': row[8],
'parent_index': row[9],
'year': row[10],
'time': row[11],
'user': row[12]
}
recently_watched.append(recent_output)
@@ -441,13 +530,15 @@ class DataFactory(object):
metadata = {}
for item in result:
directors = item['directors'].split(';')
writers = item['writers'].split(';')
actors = item['actors'].split(';')
genres = item['genres'].split(';')
directors = item['directors'].split(';') if item['directors'] else []
writers = item['writers'].split(';') if item['writers'] else []
actors = item['actors'].split(';') if item['actors'] else []
genres = item['genres'].split(';') if item['genres'] else []
metadata = {'type': item['media_type'],
'rating_key': item['rating_key'],
'parent_rating_key': item['parent_rating_key'],
'grandparent_rating_key': item['grandparent_rating_key'],
'grandparent_title': item['grandparent_title'],
'parent_index': item['parent_media_index'],
'parent_title': item['parent_title'],
@@ -474,4 +565,48 @@ class DataFactory(object):
'actors': actors
}
return metadata
return metadata
def delete_session_history_rows(self, row_id=None):
monitor_db = database.MonitorDatabase()
if row_id.isdigit():
logger.info(u"PlexPy DataFactory :: Deleting row id %s from the session history database." % row_id)
session_history_del = \
monitor_db.action('DELETE FROM session_history WHERE id = ?', [row_id])
session_history_media_info_del = \
monitor_db.action('DELETE FROM session_history_media_info WHERE id = ?', [row_id])
session_history_metadata_del = \
monitor_db.action('DELETE FROM session_history_metadata WHERE id = ?', [row_id])
return 'Deleted rows %s.' % row_id
else:
return 'Unable to delete rows. Input row not valid.'
def delete_all_user_history(self, user_id=None):
monitor_db = database.MonitorDatabase()
if user_id.isdigit():
logger.info(u"PlexPy DataFactory :: Deleting all history for user id %s from database." % user_id)
session_history_media_info_del = \
monitor_db.action('DELETE FROM '
'session_history_media_info '
'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id '
'FROM session_history_media_info '
'JOIN session_history ON session_history_media_info.id = session_history.id '
'WHERE session_history.user_id = ?)', [user_id])
session_history_metadata_del = \
monitor_db.action('DELETE FROM '
'session_history_metadata '
'WHERE session_history_metadata.id IN (SELECT session_history_metadata.id '
'FROM session_history_metadata '
'JOIN session_history ON session_history_metadata.id = session_history.id '
'WHERE session_history.user_id = ?)', [user_id])
session_history_del = \
monitor_db.action('DELETE FROM '
'session_history '
'WHERE session_history.user_id = ?', [user_id])
return 'Deleted all items for user_id %s.' % user_id
else:
return 'Unable to delete items. Input user_id not valid.'

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# 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
@@ -300,6 +300,7 @@ def build_notify_text(session, state):
duration = helpers.convert_milliseconds_to_minutes(item_metadata['duration'])
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
stream_duration = 0 if state == 'play' else int((time.time() - helpers.cast_to_float(session['started']) - helpers.cast_to_float(session['paused_counter'])) / 60)
progress_percent = helpers.get_percent(view_offset, duration)
@@ -317,10 +318,13 @@ def build_notify_text(session, state):
'content_rating': item_metadata['content_rating'],
'summary': item_metadata['summary'],
'season_num': item_metadata['parent_index'],
'season_num00': item_metadata['parent_index'].zfill(2),
'episode_num': item_metadata['index'],
'episode_num00': item_metadata['index'].zfill(2),
'album_name': item_metadata['parent_title'],
'rating': item_metadata['rating'],
'duration': duration,
'stream_duration': stream_duration,
'progress': view_offset,
'progress_percent': progress_percent
}

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# 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
@@ -24,13 +24,22 @@ class Users(object):
def get_user_list(self, kwargs=None):
data_tables = datatables.DataTables()
columns = ['users.user_id as user_id',
'users.custom_avatar_url as thumb',
columns = ['session_history.id',
'users.user_id as user_id',
'users.custom_avatar_url as user_thumb',
'(case when users.friendly_name is null then users.username else \
users.friendly_name end) as friendly_name',
'MAX(session_history.started) as last_seen',
'session_history.ip_address as ip_address',
'COUNT(session_history.id) as plays',
'session_history.player as platform',
'session_history_metadata.full_title as last_watched',
'session_history_metadata.thumb',
'session_history_metadata.parent_thumb',
'session_history_metadata.grandparent_thumb',
'session_history_metadata.media_type',
'session_history.rating_key as rating_key',
'session_history_media_info.video_decision',
'users.username as user'
]
try:
@@ -38,9 +47,15 @@ class Users(object):
columns=columns,
custom_where=[],
group_by=['users.user_id'],
join_types=['LEFT OUTER JOIN'],
join_tables=['session_history'],
join_evals=[['session_history.user_id', 'users.user_id']],
join_types=['LEFT OUTER JOIN',
'LEFT OUTER JOIN',
'LEFT OUTER JOIN'],
join_tables=['session_history',
'session_history_metadata',
'session_history_media_info'],
join_evals=[['session_history.user_id', 'users.user_id'],
['session_history.id', 'session_history_metadata.id'],
['session_history.id', 'session_history_media_info.id']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
@@ -54,16 +69,30 @@ class Users(object):
rows = []
for item in users:
if not item['thumb'] or item['thumb'] == '':
if item["media_type"] == 'episode' and item["parent_thumb"]:
thumb = item["parent_thumb"]
elif item["media_type"] == 'episode':
thumb = item["grandparent_thumb"]
else:
thumb = item["thumb"]
if not item['user_thumb'] or item['user_thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_thumb = item['user_thumb']
row = {"plays": item['plays'],
row = {"id": item['id'],
"plays": item['plays'],
"last_seen": item['last_seen'],
"friendly_name": item["friendly_name"],
"ip_address": item["ip_address"],
"thumb": user_thumb,
"friendly_name": item['friendly_name'],
"ip_address": item['ip_address'],
"platform": item['platform'],
"last_watched": item['last_watched'],
"thumb": thumb,
"media_type": item['media_type'],
"rating_key": item['rating_key'],
"video_decision": item['video_decision'],
"user_thumb": user_thumb,
"user": item["user"],
"user_id": item['user_id']
}
@@ -81,13 +110,25 @@ class Users(object):
def get_user_unique_ips(self, kwargs=None, custom_where=None):
data_tables = datatables.DataTables()
columns = ['session_history.started as last_seen',
# Change custom_where column name due to ambiguous column name after JOIN
custom_where[0][0] = 'custom_user_id' if custom_where[0][0] == 'user_id' else custom_where[0][0]
columns = ['session_history.id',
'session_history.started as last_seen',
'session_history.ip_address as ip_address',
'COUNT(session_history.id) as play_count',
'session_history.player as platform',
'session_history_metadata.full_title as last_watched',
'session_history_metadata.thumb',
'session_history_metadata.parent_thumb',
'session_history_metadata.grandparent_thumb',
'session_history_metadata.media_type',
'session_history.rating_key as rating_key',
'session_history_media_info.video_decision',
'session_history.user as user',
'session_history.user_id as user_id'
'session_history.user_id as custom_user_id',
'(case when users.friendly_name is null then users.username else \
users.friendly_name end) as friendly_name'
]
try:
@@ -95,9 +136,15 @@ class Users(object):
columns=columns,
custom_where=custom_where,
group_by=['ip_address'],
join_types=['JOIN'],
join_tables=['session_history_metadata'],
join_evals=[['session_history.id', 'session_history_metadata.id']],
join_types=['JOIN',
'JOIN',
'JOIN'],
join_tables=['users',
'session_history_metadata',
'session_history_media_info'],
join_evals=[['session_history.user_id', 'users.user_id'],
['session_history.id', 'session_history_metadata.id'],
['session_history.id', 'session_history_media_info.id']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
@@ -111,11 +158,24 @@ class Users(object):
rows = []
for item in results:
row = {"last_seen": item['last_seen'],
if item["media_type"] == 'episode' and item["parent_thumb"]:
thumb = item["parent_thumb"]
elif item["media_type"] == 'episode':
thumb = item["grandparent_thumb"]
else:
thumb = item["thumb"]
row = {"id": item['id'],
"last_seen": item['last_seen'],
"ip_address": item['ip_address'],
"play_count": item['play_count'],
"platform": item['platform'],
"last_watched": item['last_watched']
"last_watched": item['last_watched'],
"thumb": thumb,
"media_type": item['media_type'],
"rating_key": item['rating_key'],
"video_decision": item['video_decision'],
"friendly_name": item['friendly_name']
}
rows.append(row)

View File

@@ -1 +1,2 @@
PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.1.3"

View File

@@ -241,3 +241,37 @@ def update():
e
)
return
def read_changelog():
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
try:
logfile = open(changelog_file, "r")
except IOError, e:
logger.error('PlexPy Version Checker :: Unable to open changelog file. %s' % e)
return None
if logfile:
output = ''
lines = logfile.readlines()
previous_line = ''
for line in lines:
if line[:2] == '# ':
output += '<h3>' + line[2:] + '</h3>'
elif line[:3] == '## ':
output += '<h4>' + line[3:] + '</h4>'
elif line[:2] == '* ' and previous_line.strip() == '':
output += '<ul><li>' + line[2:] + '</li>'
elif line[:2] == '* ':
output += '<li>' + line[2:] + '</li>'
elif line.strip() == '' and previous_line[:2] == '* ':
output += '</ul></br>'
else:
output += line + '</br>'
previous_line = line
return output
else:
return '<h4>No changelog data</h4>'

View File

@@ -65,7 +65,8 @@ class WebInterface(object):
@cherrypy.expose
def home(self):
config = {
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE
}
return serve_template(templatename="index.html", title="Home", config=config)
@@ -118,9 +119,9 @@ class WebInterface(object):
return json.dumps(formats)
@cherrypy.expose
def home_stats(self, time_range='30', **kwargs):
def home_stats(self, time_range='30', stat_type='0', **kwargs):
data_factory = datafactory.DataFactory()
stats_data = data_factory.get_home_stats(time_range=time_range)
stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type)
return serve_template(templatename="home_stats.html", title="Stats", data=stats_data)
@@ -451,6 +452,7 @@ class WebInterface(object):
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE),
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT
}
@@ -473,7 +475,7 @@ class WebInterface(object):
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start",
"tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop",
"tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup",
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote"
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
@@ -1256,3 +1258,32 @@ class WebInterface(object):
return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers",
data=this_agent)
@cherrypy.expose
def delete_history_rows(self, row_id, **kwargs):
data_factory = datafactory.DataFactory()
if row_id:
delete_row = data_factory.delete_session_history_rows(row_id=row_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})
@cherrypy.expose
def delete_all_user_history(self, user_id, **kwargs):
data_factory = datafactory.DataFactory()
if user_id:
delete_row = data_factory.delete_all_user_history(user_id=user_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})