Compare commits

..

53 Commits

Author SHA1 Message Date
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
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
Tim
cea4992331 v1.1.2 2015-08-16 13:02:38 +02:00
Tim
c98a8865d6 Fix bug where user refresh would fail under certain circumstances.
Move all user functions to it's own class.
2015-08-16 12:40:32 +02:00
44 changed files with 2789 additions and 1661 deletions

View File

@@ -1,13 +1,34 @@
# Changelog # 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 bug where user refresh would fail under certain circumstances.
* Fix typo on graphs page causing date selection to break on Safari.
## 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) ## v1.1.0 (2015-08-15)
@@ -24,9 +45,11 @@
* Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close. * Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close.
* Fix some styling niggles. * 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. * Allow SSL certificate check override for certain systems with bad CA stores.
* Added TV show title to recently added text. Thanks @jroyal. * Fix typo on graphs page causing date selection to break on Safari.
* 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.0 (2015-08-11)
* 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 * 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) [![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)
@@ -35,48 +35,63 @@ If you'd like to buy me a beer, hit the donate button below.
* video type & resolution * video type & resolution
* audio type & channel count. * 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 * Recently added media and how long ago it was added
* Global watching history with search/filtering & dynamic column sorting * Global watching history with search/filtering & dynamic column sorting
* date * date
* user * user
* platform * platform
* ip address (if enabled in plexWatch) * ip address
* title * title
* stream information details * stream information details
* start time * start time
* paused duration length * paused duration length
* stop time * stop time
* duration length * 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 * Full user list with general information and comparison stats
* Individual user information * Individual user information
- username and gravatar (if available) * username and gravatar (if available)
- daily, weekly, monthly, all time stats for play count and duration length * daily, weekly, monthly, all time stats for play count and duration length
- individual platform stats for each user * individual platform stats for each user
- public ip address history with last seen date and geo tag location * public ip address history with last seen date and geo tag location
- recently watched content * recently watched content
- watching history * watching history
- synced items * 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 * Rich analytics presented using Highcharts graphing
- user-selectable time periods of 30, 90 or 365 days * user-selectable time periods of 30, 90 or 365 days
- daily watch count and duration * daily watch count and duration
- totals by day of week and hours of the day * totals by day of week and hours of the day
- totals by top 10 platform * totals by top 10 platform
- totals by top 10 users * totals by top 10 users
- detailed breakdown by transcode decision * detailed breakdown by transcode decision
- source and stream resolutions * source and stream resolutions
- transcode decision counts by user and platform * transcode decision counts by user and platform
- total monthly counts * total monthly counts
* Content information pages * Content information pages
- movies (includes watching history) * movies (includes watching history)
- tv shows (includes watching history) * tv shows (includes watching history)
- tv seasons * tv seasons
- tv episodes (includes watching history) * tv episodes (includes watching history)
* Full sync list data on all users syncing items from your library * Full sync list data on all users syncing items from your library

View File

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

View File

@@ -251,15 +251,9 @@ fieldset[disabled] .btn-bright.active {
background-color: #222222; background-color: #222222;
} }
.modal-body table { .modal-body table {
color: #999; color: #fff;
}
.modal-body ul {
list-style: none;
-webkit-padding-start: 0px;
margin: 0;
} }
.modal-body li { .modal-body li {
list-style: none;
margin-top: 7px; margin-top: 7px;
margin-left: 4px; margin-left: 4px;
color: #aaa; color: #aaa;
@@ -270,6 +264,9 @@ fieldset[disabled] .btn-bright.active {
.modal-body i { .modal-body i {
color: #F9AA03; color: #F9AA03;
} }
.modal-body i.fa {
color: #fff;
}
.modal-body strong { .modal-body strong {
color: #F9AA03; color: #F9AA03;
} }
@@ -407,39 +404,31 @@ input[type="color"],
} }
.poster { .poster {
float: left; float: left;
min-height: 232px; min-height: 225px;
min-width: 155px; min-width: 150px;
margin-bottom: 8px;
position: relative; position: relative;
} }
.poster-face img { .poster-face {
bottom: 0; background-position: center;
overflow: hidden; background-size: cover;
height: 225px; height: 225px;
width: 153px; width: 150px;
position: relative;
webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
}
.cover-face {
background-position: center;
background-size: cover;
height: 150px;
width: 150px;
position: absolute; position: absolute;
bottom: 5px;
left: 0;
border: 1px solid rgba(128, 128, 128, 0.3);
}
.poster-face img:hover {
webkit-box-shadow: 0 0 0 2px #e9a049;
-moz-box-shadow: 0 0 0 2px #e9a049;
box-shadow: 0 0 0 2px #e9a049;
}
.cover-face img {
bottom: 0; bottom: 0;
overflow: hidden; webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
height: 153px; -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
width: 153px; box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
border: 1px solid rgba(128, 128, 128, 0.3);
position: absolute;
bottom: 5px;
left: 0;
}
.cover-face img:hover {
webkit-box-shadow: 0 0 0 2px #e9a049;
-moz-box-shadow: 0 0 0 2px #e9a049;
box-shadow: 0 0 0 2px #e9a049;
} }
.users-poster-face img { .users-poster-face img {
bottom: 0; bottom: 0;
@@ -524,7 +513,7 @@ input[type="color"],
left: 0; left: 0;
float: left; float: left;
text-align: left; text-align: left;
padding: 2px 2px 6px 2px; padding: 5px 8px;
font-size: 12px; font-size: 12px;
color: #eee; color: #eee;
text-overflow: ellipsis; text-overflow: ellipsis;
@@ -538,7 +527,7 @@ input[type="color"],
right: 0; right: 0;
float: right; float: right;
text-align: right; text-align: right;
padding: 2px 2px 6px 2px; padding: 5px 8px;
font-size: 12px; font-size: 12px;
color: #eee; color: #eee;
} }
@@ -630,10 +619,16 @@ input[type="color"],
} }
.dashboard-recent-media-instance { .dashboard-recent-media-instance {
} }
.dashboard-recent-media-instance a:hover .poster-face {
webkit-box-shadow: inset 0 0 0 2px #e9a049;
-moz-box-shadow: inset 0 0 0 2px #e9a049;
box-shadow: inset 0 0 0 2px #e9a049;
}
.dashboard-recent-media li { .dashboard-recent-media li {
margin-right: 27px; margin-right: 27px;
position: relative; position: relative;
float: left; float: left;
min-height: 340px;
} }
.dashboard-recent-media-metacontainer{ .dashboard-recent-media-metacontainer{
width: 153px; width: 153px;
@@ -642,7 +637,7 @@ input[type="color"],
clear: both; clear: both;
} }
.dashboard-recent-media-metacontainer h3 { .dashboard-recent-media-metacontainer h3 {
padding-top: 5px; padding: 5px 3px 0 3px;
color: #fff; color: #fff;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
@@ -650,18 +645,19 @@ input[type="color"],
font-size: 13px; font-size: 13px;
margin: 0; margin: 0;
line-height: 15px; line-height: 15px;
font-weight: bold; font-weight: normal;
width: 153px; width: 153px;
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: left;
clear: both; clear: both;
} }
.dashboard-recent-media-metacontainer text-muted { .dashboard-recent-media-metacontainer .text-muted {
padding: 5px 3px 0 3px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
white-space: nowrap; white-space: nowrap;
text-align: center; text-align: left;
} }
.art-face { .art-face {
background-repeat: no-repeat; background-repeat: no-repeat;
@@ -862,6 +858,25 @@ input[type="color"],
} }
.summary-content-director strong { .summary-content-director strong {
color: #fff; color: #fff;
margin-left: 2px;
}
.summary-content-studio {
float: left;
margin-right: 10px;
line-height: 24px;
}
.summary-content-studio strong {
color: #fff;
margin-left: 2px;
}
.summary-content-airdate {
float: left;
margin-right: 10px;
line-height: 24px;
}
.summary-content-airdate strong {
color: #fff;
margin-left: 2px;
} }
.summary-content-duration { .summary-content-duration {
float: left; float: left;
@@ -870,6 +885,7 @@ input[type="color"],
} }
.summary-content-duration strong { .summary-content-duration strong {
color: #fff; color: #fff;
margin-left: 2px;
} }
.summary-content-content-rating { .summary-content-content-rating {
float: left; float: left;
@@ -878,6 +894,7 @@ input[type="color"],
} }
.summary-content-content-rating strong { .summary-content-content-rating strong {
color: #fff; color: #fff;
margin-left: 2px;
} }
.summary-content-summary { .summary-content-summary {
overflow: hidden; overflow: hidden;
@@ -890,6 +907,9 @@ input[type="color"],
max-height: 160px; max-height: 160px;
padding-bottom: 0px; padding-bottom: 0px;
} }
.summary-content-people-wrapper {
margin-top: 25px;
}
.summary-content-people-wrapper hidden-phone hidden-tablet { .summary-content-people-wrapper hidden-phone hidden-tablet {
overflow: hidden; overflow: hidden;
height: auto; height: auto;
@@ -897,13 +917,16 @@ input[type="color"],
} }
.summary-content-actors { .summary-content-actors {
margin-top: 0px; margin-top: 0px;
margin-left: 35px; margin-left: 0px;
margin-right: 35px; margin-right: 15px;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: #999; color: #999;
overflow: hidden; overflow: hidden;
} }
.summary-content-actors ul {
padding-left:20px;
}
.summary-content-actors li { .summary-content-actors li {
list-style: none; list-style: none;
list-style-image: none; list-style-image: none;
@@ -912,15 +935,38 @@ input[type="color"],
line-height: 18px; line-height: 18px;
color: #fff; color: #fff;
} }
.summary-content-writers { .summary-content-genres {
margin-top: 10px; margin-top: 0px;
margin-left: 35px; margin-left: 0px;
margin-right: 35px; margin-right: 15px;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: #999; color: #999;
overflow: hidden; overflow: hidden;
} }
.summary-content-genres ul {
padding-left:20px;
}
.summary-content-genres li {
list-style: none;
list-style-image: none;
display: block;
font-size: 12px;
line-height: 18px;
color: #fff;
}
.summary-content-writers {
margin-top: 0px;
margin-left: 0px;
margin-right: 15px;
font-size: 12px;
line-height: 18px;
color: #999;
overflow: hidden;
}
.summary-content-writers ul {
padding-left: 20px;
}
.summary-content-writers li { .summary-content-writers li {
list-style: none; list-style: none;
list-style-image: none; list-style-image: none;
@@ -959,18 +1005,32 @@ input[type="color"],
list-style: none; list-style: none;
margin: 0 0 0px 0px; margin: 0 0 0px 0px;
} }
.season-episodes-instance>li { .season-episodes-instance li {
float: left; float: left;
height: 200px;
width: 250px;
position: relative; position: relative;
left: 0px; left: 0px;
margin-right: 25px;
}
.season-episodes-instance a:hover .season-episodes-card-overlay {
webkit-box-shadow: inset 0 0 0 2px #e9a049;
-moz-box-shadow: inset 0 0 0 2px #e9a049;
box-shadow: inset 0 0 0 2px #e9a049;
} }
.season-episodes-poster { .season-episodes-poster {
float: left; float: left;
position: relative; position: relative;
left: 0px; left: 0px;
} }
.season-episodes-poster-face {
background-position: center;
background-size: cover;
height: 140px;
width: 250px;
position: relative;
webkit-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
}
.season-episodes-poster-face img { .season-episodes-poster-face img {
bottom: 0; bottom: 0;
overflow: hidden; overflow: hidden;
@@ -987,11 +1047,14 @@ input[type="color"],
left: 0; left: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
padding: 4px 8px;
text-align: left; text-align: left;
background-color: #000; background: -moz-linear-gradient(top, rgba(0,0,0,0) 30%, rgba(0,0,0,1) 100%);
background-color: rgba(0,0,0,0.7); background: -webkit-gradient(linear, left top, left bottom, color-stop(30%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1)));
border-top: 1px solid #000; background: -webkit-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%);
background: -o-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%);
background: -ms-linear-gradient(top, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%);
background: linear-gradient(to bottom, rgba(0,0,0,0) 30%,rgba(0,0,0,1) 100%);
height: 140px;
} }
.season-episodes-card-overlay-index { .season-episodes-card-overlay-index {
color: #fff; color: #fff;
@@ -999,10 +1062,27 @@ input[type="color"],
text-shadow: 0 1px 5px rgba(0,0,0,0.2); text-shadow: 0 1px 5px rgba(0,0,0,0.2);
} }
.season-episodes-instance-text-wrapper { .season-episodes-instance-text-wrapper {
width: 250px;
font-size: 13px;
margin-bottom: 20px;
clear: both; clear: both;
width: 205px;
} }
.season-episodes-title, .season-episodes-title a { .season-episodes-instance-text-wrapper h3 {
padding: 5px 3px 0 3px;
color: #fff;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
font-size: 13px;
margin: 0;
line-height: 15px;
font-weight: normal;
width: 250px;
white-space: nowrap;
text-align: left;
clear: both;
}
.season-episodes-title a {
text-decoration: none; text-decoration: none;
font-size: 14px; font-size: 14px;
font-weight: normal; font-weight: normal;
@@ -1015,6 +1095,7 @@ input[type="color"],
width: 205px; width: 205px;
margin-top: 2px; margin-top: 2px;
margin-left: 0px; margin-left: 0px;
margin-bottom: 20px;
} }
.season-episodes a:hover { .season-episodes a:hover {
color: #F9AA03; color: #F9AA03;
@@ -1023,6 +1104,9 @@ input[type="color"],
color: #aaa; color: #aaa;
font-size: 12px; font-size: 12px;
float: left; float: left;
position: absolute;
left: 8px;
bottom: 5px;
} }
.user-info-wrapper { .user-info-wrapper {
height: 113px; height: 113px;
@@ -1078,7 +1162,10 @@ input[type="color"],
margin: 0px 0px 0px 0px; margin: 0px 0px 0px 0px;
} }
.user-overview-stats-instance { .user-overview-stats-instance {
line-height: 11px; float: left;
width: 350px;
height: 80px;
margin-bottom: 25px;
} }
.user-overview-stats-instance-device_icon { .user-overview-stats-instance-device_icon {
float: left; float: left;
@@ -1100,25 +1187,27 @@ input[type="color"],
.user-overview-stats-instance p { .user-overview-stats-instance p {
color: #aaa; color: #aaa;
font-size: 12px; font-size: 12px;
float:left; float: left;
position: relative; position: relative;
top: 30px; top: 14px;
left: 0px; left: 0px;
} }
.user-overview-stats-instance h3 strong{
color: #fff;
}
.user-overview-stats-instance h3 { .user-overview-stats-instance h3 {
font-size: 30px; font-size: 30px;
font-weight: bold; font-weight: bold;
color: #F9AA03; color: #F9AA03;
line-height: 30px; line-height: 22px;
position: relative; position: relative;
top: 0px; top: 5px;
margin-left: 5px; margin: 0 5px 0 10px;
margin-right: 5px;
float: left; float: left;
} }
.user-overview-stats-instance h4 { .user-overview-stats-instance h4 {
color: #fff; color: #fff;
margin-bottom: 10px; margin-bottom: 25px;
} }
.user-overview-stats-instance h1 { .user-overview-stats-instance h1 {
font-size: 54px; font-size: 54px;
@@ -1138,7 +1227,7 @@ input[type="color"],
.user-platforms-instance { .user-platforms-instance {
float: left; float: left;
width: 240px; width: 240px;
height: 90px; height: 80px;
margin-bottom: 25px; margin-bottom: 25px;
} }
.user-platforms-instance li { .user-platforms-instance li {
@@ -1150,6 +1239,10 @@ input[type="color"],
-webkit-box-shadow: 0 0 5px rgba(0,0,0,0.5); -webkit-box-shadow: 0 0 5px rgba(0,0,0,0.5);
-moz-box-shadow: 0 0 5px rgba(0,0,0,0.5); -moz-box-shadow: 0 0 5px rgba(0,0,0,0.5);
box-shadow: 0 0 5px rgba(0,0,0,0.5); box-shadow: 0 0 5px rgba(0,0,0,0.5);
background-size: contain;
position: relative;
height: 80px;
width: 80px;
} }
.user-platforms-instance-name { .user-platforms-instance-name {
float: left; float: left;
@@ -1160,7 +1253,7 @@ input[type="color"],
position: relative; position: relative;
font-size: 13px; font-size: 13px;
line-height: 15px; line-height: 15px;
font-weight: bold; font-weight: normal;
width: 140px; width: 140px;
margin-left: 10px; margin-left: 10px;
} }
@@ -1179,26 +1272,39 @@ input[type="color"],
font-size: 12px; font-size: 12px;
float: left; float: left;
position: relative; position: relative;
top: 15px; top: 14px;
left: 0px; left: 0px;
} }
.home-platforms {
max-width: 1600px;
}
.home-platforms ul {
list-style: none;
margin: 0;
}
.home-platforms-instance-poster { .home-platforms-instance-poster {
float: left; margin-left: 0px;
}
.home-platforms-instance-poster .poster-face {
height: 120px; height: 120px;
max-width: 81px; width: 80px;
padding: 5px 10px 5px 10px;
} }
.home-platforms-instance-box { .home-platforms-instance-box {
float: left; background-size: contain;
position: absolute;
left: 10px;
bottom: 35px;
height: 80px; height: 80px;
width: 80px; width: 80px;
padding: 25px 10px 25px 10px; border-radius: 3px;
} }
.home-platforms-instance-oval { .home-platforms-instance-oval {
float: left; background-size: contain;
position: absolute;
left: 10px;
bottom: 35px;
height: 80px; height: 80px;
width: 80px; width: 80px;
padding: 25px 10px 25px 10px;
-webkit-border-radius: 50%; -webkit-border-radius: 50%;
-moz-border-radius: 50%; -moz-border-radius: 50%;
border-radius: 50%; border-radius: 50%;
@@ -1212,21 +1318,61 @@ input[type="color"],
font-size: 13px; font-size: 13px;
line-height: 15px; line-height: 15px;
font-weight: bold; font-weight: bold;
width: 100%;
padding: 10px 0 0 10px;
}
.home-platforms-instance-playcount {
float: left;
width: 180px; width: 180px;
margin-left: 10px; }
margin-top: 10px; .home-platforms-instance-mediainfo {
float: left;
background-color: #282828;
position: absolute;
bottom: 0;
left: 0;
padding-left: 170px;
width: 100%;
height: 175px;
}
.home-platforms-instance-media {
position: relative;
float: left;
width: 375px;
height: 225px;
padding-bottom: 10px;
margin-right: 25px;
margin-bottom: 25px;
webkit-box-sizing: content-box;
box-sizing: content-box;
}
.home-platforms-instance-info {
float: left;
background-color: #282828;
position: absolute;
top: 5px;
left: 0;
padding-left: 100px;
width: 100%;
height: 120px;
} }
.home-platforms-instance { .home-platforms-instance {
background-color: #282828;
position: relative;
float: left; float: left;
width: 300px; width: 300px;
height: 130px; height: 120px;
padding: 5px; padding: 10px;
background-color: #282828;
margin-right: 20px; margin-right: 20px;
margin-bottom: 20px; margin-bottom: 20px;
webkit-box-sizing: content-box; webkit-box-sizing: content-box;
box-sizing: content-box; box-sizing: content-box;
} }
.home-platforms-instance a:hover .poster-face {
webkit-box-shadow: inset 0 0 0 2px #e9a049;
-moz-box-shadow: inset 0 0 0 2px #e9a049;
box-shadow: inset 0 0 0 2px #e9a049;
}
.history-table-title { .history-table-title {
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
@@ -1500,6 +1646,30 @@ input[type="color"],
width: 250px; width: 250px;
z-index: 9999; z-index: 9999;
} }
.tooltip.top .tooltip-arrow {
border-top-color: #fff;
}
.tooltip.right .tooltip-arrow {
border-right-color: #fff;
}
.tooltip.bottom .tooltip-arrow {
border-bottom-color: #fff;
}
.tooltip.left .tooltip-arrow {
border-left-color: #fff;
}
.tooltip-inner {
color: #000;
background: #fff;
border: 0;
font-weight: bold;
}
.history-title .popover.right {
margin-left: 12px;
}
.history-title .popover.right .popover-content {
padding: 5px 8px;
}
#updatebar { #updatebar {
background-color: #444; background-color: #444;
color: #999999; color: #999999;

View File

@@ -44,7 +44,7 @@ DOCUMENTATION :: END
<input type="text" class="form-control" id="profile_url" name="profile_url" value="${data['thumb']}"> <input type="text" class="form-control" id="profile_url" name="profile_url" value="${data['thumb']}">
</div> </div>
</div> </div>
<p class="help-block">Change the users profile picture in PlexPy.</p> <p class="help-block">Change the users profile picture in PlexPy. To reset to default, leave this field empty and save then perform a user refresh.</p>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
@@ -58,6 +58,12 @@ DOCUMENTATION :: END
</label> </label>
<p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p> <p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p>
</div> </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> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -112,6 +118,21 @@ DOCUMENTATION :: END
}); });
% endif % 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> </script>
% endif % endif

View File

@@ -263,10 +263,37 @@
<script> <script>
$(document).ready(function () { $(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 yaxis = 'plays';
var current_range = 30;
var current_tab = '#tabs-1'; 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) { function loadGraphsTab1(time_range, yaxis) {
setGraphFormat(yaxis); setGraphFormat(yaxis);
@@ -432,12 +459,8 @@
data: { y_axis: yaxis }, data: { y_axis: yaxis },
dataType: "json", dataType: "json",
success: function(data) { 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.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; hc_plays_by_month_options.series = data.series;
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options); var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
} }

View File

@@ -13,23 +13,26 @@
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-history"></i> History</span> <span><i class="fa fa-history"></i> History</span>
</div> </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> </div>
<div class='table-card-back'> <div class='table-card-back'>
<table class="display" id="history_table" width="100%"> <table class="display" id="history_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align='left' id="delete_row">Delete</th>
<th align='left' id="time">Time</th> <th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</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="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th> <th align='left' id="title">Title</th>
<th align='left' id="started">Started</th> <th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th> <th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th> <th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th> <th align='left' id="percent_complete"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -60,8 +63,22 @@
} }
} }
history_table = $('#history_table').DataTable(history_table_options); 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'); $(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> </script>

View File

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

View File

@@ -9,7 +9,8 @@ Variable names: data [array]
data[array_index] :: Usable parameters 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['rows'] Returns an array containing stat data
data[array_index]['rows'] :: Usable parameters 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. rating_key Returns the unique identifier for the media item.
title Returns the title for the associated stat. 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_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. users_watched Returns the count for the associated stat.
== Only if 'stat_id' is 'top_user' == == Only if 'stat_id' is 'top_user' ==
@@ -39,6 +41,22 @@ platform_type Returns the platform name for the associated stat.
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%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:
% if data[0]['rows'] or data[2]['rows']: % if data[0]['rows'] or data[2]['rows']:
<ul class="list-unstyled"> <ul class="list-unstyled">
@@ -46,16 +64,7 @@ DOCUMENTATION :: END
% if a['stat_id'] == 'top_tv' and a['rows']: % if a['stat_id'] == 'top_tv' and a['rows']:
<div class="home-platforms-instance"> <div class="home-platforms-instance">
<li> <li>
<span> <div class="home-platforms-instance-info">
<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"> <div class="home-platforms-instance-name">
<h4>Most Watched TV</h4> <h4>Most Watched TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}"> <h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
@@ -63,24 +72,31 @@ DOCUMENTATION :: END
</a></h5> </a></h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div> </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> </li>
</div> </div>
% elif a['stat_id'] == 'popular_tv' and a['rows']: % elif a['stat_id'] == 'popular_tv' and a['rows']:
<div class="home-platforms-instance"> <div class="home-platforms-instance">
<li> <li>
<span> <div class="home-platforms-instance-info">
<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"> <div class="home-platforms-instance-name">
<h4>Most Popular TV</h4> <h4>Most Popular TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}"> <h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
@@ -91,21 +107,24 @@ DOCUMENTATION :: END
<h3>${a['rows'][0]['users_watched']}</h3> <h3>${a['rows'][0]['users_watched']}</h3>
<p> users</p> <p> users</p>
</div> </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> </li>
</div> </div>
% elif a['stat_id'] == 'top_movies' and a['rows']: % elif a['stat_id'] == 'top_movies' and a['rows']:
<div class="home-platforms-instance"> <div class="home-platforms-instance">
<li> <li>
<span> <div class="home-platforms-instance-info">
<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">
% else:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name"> <div class="home-platforms-instance-name">
<h4>Most Watched Movie</h4> <h4>Most Watched Movie</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}"> <h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
@@ -113,29 +132,59 @@ DOCUMENTATION :: END
</a></h5> </a></h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div> </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'] == '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> </li>
</div> </div>
% elif a['stat_id'] == 'top_users' and a['rows']: % elif a['stat_id'] == 'top_users' and a['rows']:
<div class="home-platforms-instance"> <div class="home-platforms-instance">
<li> <li>
<span> <div class="home-platforms-instance-info">
% 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">
% else:
<img class="home-platforms-instance-oval"
src="interfaces/default/images/gravatar-default.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name"> <div class="home-platforms-instance-name">
<h4>Most Active User</h4> <h4>Most Active User</h4>
<h5> <h5>
@@ -149,29 +198,55 @@ DOCUMENTATION :: END
</h5> </h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div> </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> </li>
</div> </div>
% elif a['stat_id'] == 'top_platforms' and a['rows']: % elif a['stat_id'] == 'top_platforms' and a['rows']:
<div class="home-platforms-instance"> <div class="home-platforms-instance">
<li> <li>
<div id="platform-stat"> <div class="home-platforms-instance-info">
<img class="home-platforms-instance-box" src="interfaces/default/images/platforms/default.png">
</div>
<div class="home-platforms-instance-name"> <div class="home-platforms-instance-name">
<h4>Most Active Platform</h4> <h4>Most Active Platform</h4>
<h5>${a['rows'][0]['platform_type']}</h5> <h5>${a['rows'][0]['platform_type']}</h5>
</div> </div>
<div class="user-platforms-instance-playcount"> <div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3> <h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p> <p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
<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> </div>
</li> </li>
</div> </div>
<script> <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> </script>
% endif % endif
% endfor % endfor

View File

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

View File

@@ -30,6 +30,7 @@ genres Returns an array of genres.
actors Returns an array of actors. actors Returns an array of actors.
directors Returns an array of directors. directors Returns an array of directors.
studio Returns the name of the studio. studio Returns the name of the studio.
originally_available_at Returns the air date of the item.
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%doc>
@@ -53,46 +54,58 @@ DOCUMENTATION :: END
<div class="row"> <div class="row">
<div class="col-md-9"> <div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm"> <div class="summary-content-poster hidden-xs hidden-sm">
% if data['type'] == 'episode': % if data['type'] == 'episode' and data['parent_thumb']:
<img src="pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster"> <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: % 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 % endif
</div> </div>
<div class="summary-content"> <div class="summary-content">
<div class="summary-content-title"> <div class="summary-content-title">
% if data['type'] == 'movie': % if data['type'] == 'movie':
<h1>${data['title']} (${data['year']})</h1> <h1>${data['title']}</h1>
% elif data['type'] == 'season': % elif data['type'] == 'season':
<h1>${data['parent_title']} (${data['title']})</h1> <h1>${data['parent_title']} (${data['title']})</h1>
% elif data['type'] == 'episode': % elif data['type'] == 'episode':
<h1>${data['grandparent_title']} (Season ${data['parent_index']}, Episode <h1>${data['grandparent_title']} - ${data['title']}
${data['index']}) "${data['title']}"</h1> (Season ${data['parent_index']}, Episode ${data['index']})</h1>
% else: % else:
<h1>${data['title']}</h1> <h1>${data['title']}</h1>
% endif % endif
</div> </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="" <div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
data-rateit-ispreset="true" data-rateit-readonly="true"></div> data-rateit-ispreset="true" data-rateit-readonly="true"></div>
% endif % endif
<div class="summary-content-details-wrapper"> <div class="summary-content-details-wrapper">
<div class="summary-content-director"> <div class="summary-content-director">
% if data['type'] == 'episode' or data['type'] == 'movie': % if (data['type'] == 'episode' or data['type'] == 'movie') and data['directors']:
% if data['directors']:
Directed by <strong> ${data['directors'][0]}</strong> Directed by <strong> ${data['directors'][0]}</strong>
% else:
Directed by <strong> unknown</strong>
% endif % 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> Studio <strong> ${data['studio']}</strong>
% endif % endif
</div> </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"> <div class="summary-content-duration">
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong> Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
</div> </div>
<div class="summary-content-content-rating"> <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> Rated <strong> ${data['content_rating']} </strong>
% endif
</div> </div>
</div> </div>
<div class="summary-content-summary"> <div class="summary-content-summary">
@@ -100,9 +113,23 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% if data['type'] == 'episode':
<div class="col-md-3"> <div class="col-md-3">
<div class="summary-content-people-wrapper hidden-xs hidden-sm"> <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"> <div class="summary-content-writers">
<strong>Written by</strong> <strong>Written by</strong>
<ul> <ul>
@@ -115,24 +142,8 @@ DOCUMENTATION :: END
% endfor % endfor
</ul> </ul>
</div> </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">
<div class="summary-content-actors">
<strong>Genres</strong>
<ul>
% for genre in data['genres']:
% if loop.index < 5:
<li>
${genre}
</li>
% endif % endif
% endfor % if (data['type'] == 'movie' or data['type'] == 'show') and data['actors']:
</ul>
</div>
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
<div class="summary-content-actors"> <div class="summary-content-actors">
<strong>Starring</strong> <strong>Starring</strong>
<ul> <ul>
@@ -145,11 +156,6 @@ DOCUMENTATION :: END
% endfor % endfor
</ul> </ul>
</div> </div>
</div>
</div>
</div>
% elif data['type'] == 'season':
<div class="col-md-3"></div>
% endif % endif
</div> </div>
</div> </div>
@@ -157,6 +163,8 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
</div>
</div>
</div> </div>
% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show': % if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show':
<div class='container-fluid'> <div class='container-fluid'>
@@ -164,25 +172,28 @@ DOCUMENTATION :: END
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
<span>Watch history for <strong>${data['title']}</strong></span> <span>Watch History for <strong>${data['title']}</strong></span>
</div> </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> </div>
<div class="table-card-back"> <div class="table-card-back">
<table class="display" id="history_table" width="100%"> <table class="display" id="history_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th> <th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</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="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th> <th align='left' id="title">Title</th>
<th align='left' id="started">Started</th> <th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th> <th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th> <th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th> <th align='left' id="percent_complete"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@@ -202,7 +213,7 @@ DOCUMENTATION :: END
<div class='col-md-12'> <div class='col-md-12'>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <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> </div>
<div class='table-card-back'> <div class='table-card-back'>
@@ -233,7 +244,7 @@ DOCUMENTATION :: END
<script src="interfaces/default/js/moment-with-locale.js"></script> <script src="interfaces/default/js/moment-with-locale.js"></script>
% if data: % if data:
% if data['type'] == 'movie': % if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode':
<script> <script>
// Convert rating to 5 star rating type // Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2) var starRating = Math.round(${data['rating']} / 2)
@@ -254,10 +265,23 @@ DOCUMENTATION :: END
} }
} }
history_table = $('#history_table').DataTable(history_table_options); 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'); $(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> </script>
% elif data['type'] == 'show': % elif data['type'] == 'show':
@@ -274,8 +298,22 @@ DOCUMENTATION :: END
} }
} }
history_table = $('#history_table').DataTable(history_table_options); 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'); $(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> </script>
% endif % endif
@@ -294,6 +332,7 @@ DOCUMENTATION :: END
</script> </script>
% endif % endif
<script> <script>
$("#airdate").html(moment($("#airdate")).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").html(), true)); $("#runtime").html(millisecondsToMinutes($("#runtime").html(), true));
</script> </script>
% endif % endif

View File

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

View File

@@ -15,7 +15,7 @@
<div class="modal-body" id="modal-text"> <div class="modal-body" id="modal-text">
<div class="col-md-6"> <div class="col-md-6">
<h4><strong>Location Details</strong></h4> <h4><strong>Location Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>Country: <strong><span id="country"></span></strong></li> <li>Country: <strong><span id="country"></span></strong></li>
<li>City: <strong><span id="city"></span></strong></li> <li>City: <strong><span id="city"></span></strong></li>
<li>Region: <strong><span id="region"></span></strong></li> <li>Region: <strong><span id="region"></span></strong></li>
@@ -26,7 +26,7 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h4><strong>Connection Details</strong></h4> <h4><strong>Connection Details</strong></h4>
<ul> <ul class="list-unstyled">
<li>ISP: <strong><span id="isp"></span></strong></li> <li>ISP: <strong><span id="isp"></span></strong></li>
<li>Organization: <strong><span id="org"></span></strong></li> <li>Organization: <strong><span id="org"></span></strong></li>
<li>AS: <strong><span id="as"></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) { } else if (seconds >= 60) {
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>'; text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
return text; 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'); $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, "processing": false,
"serverSide": true, "serverSide": true,
"pageLength": 25, "pageLength": 25,
"order": [ 0, 'desc'], "order": [ 1, 'desc'],
"autoWidth": false,
"columnDefs": [ "columnDefs": [
{ {
"targets": [0], "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", "data":"date",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (rowData['stopped'] === null) { if (rowData['stopped'] === null) {
@@ -38,10 +50,11 @@ history_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"width": "8%",
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [1], "targets": [2],
"data":"friendly_name", "data":"friendly_name",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@@ -54,21 +67,12 @@ history_table_options = {
$(td).html(cellData); $(td).html(cellData);
} }
}, },
"width": "8%",
"className": "no-wrap hidden-xs" "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], "targets": [3],
"data":"ip_address", "data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData) { if (cellData) {
if (isPrivateIP(cellData)) { if (isPrivateIP(cellData)) {
@@ -78,35 +82,63 @@ history_table_options = {
$(td).html('n/a'); $(td).html('n/a');
} }
} else { } 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 { } else {
$(td).html('n/a'); $(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], "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", "data":"full_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') { var media_type = '';
var transcode_dec = ''; var thumb_popover = '';
if (rowData['video_decision'] === 'transcode') { if (rowData['media_type'] === 'movie') {
transcode_dec = '<i class="fa fa-server"></i>&nbsp'; 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><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>'); $(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') { } 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 { } else {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>'); $(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
} }
} }
} },
"width": "35%"
}, },
{ {
"targets": [5], "targets": [6],
"data":"started", "data":"started",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@@ -116,10 +148,11 @@ history_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"width": "5%",
"className": "no-wrap hidden-sm hidden-xs" "className": "no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [6], "targets": [7],
"data":"paused_counter", "data":"paused_counter",
"render": function ( data, type, full ) { "render": function ( data, type, full ) {
if (data !== null) { if (data !== null) {
@@ -129,10 +162,11 @@ history_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"className": "no-wrap hidden-xs" "width": "5%",
"className": "no-wrap hidden-md hidden-sm hidden-xs"
}, },
{ {
"targets": [7], "targets": [8],
"data":"stopped", "data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
@@ -142,10 +176,11 @@ history_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"className": "no-wrap hidden-md hidden-xs" "width": "5%",
"className": "no-wrap hidden-sm hidden-xs"
}, },
{ {
"targets": [8], "targets": [9],
"data":"duration", "data":"duration",
"render": function ( data, type, full ) { "render": function ( data, type, full ) {
if (data !== null) { if (data !== null) {
@@ -155,34 +190,50 @@ history_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"width": "5%",
"className": "no-wrap hidden-xs" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [9], "targets": [10],
"data":"percent_complete", "data":"percent_complete",
"render": function ( data, type, full ) { "render": function ( data, type, full ) {
if (data > 80) { 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) { } 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 { } 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, "searchable": false,
"orderable": false, "orderable": false,
"className": "no-wrap hidden-md hidden-xs", "className": "no-wrap hidden-md hidden-sm hidden-xs",
"width": "10px" "width": "1%"
} },
], ],
"drawCallback": function (settings) { "drawCallback": function (settings) {
// Jump to top of page // Jump to top of page
// $('html,body').scrollTop(0); // $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut(); $('#ajaxMsg').fadeOut();
// Create the tooltips. // Create the tooltips.
$('.info-modal').each(function() { $('.transcode-tooltip').tooltip();
$(this).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) { "preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>"; 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']); 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], "targets": [3],
"data":"player", "data":"player",
"className": "modal-control no-wrap hidden-sm hidden-xs" "className": "no-wrap hidden-sm hidden-xs modal-control"
}, },
{ {
"targets": [4], "targets": [4],

View File

@@ -24,13 +24,11 @@ user_ip_table_options = {
}, },
"searchable": false, "searchable": false,
"width": "15%", "width": "15%",
"className": "no-wrap" "className": "no-wrap hidden-xs"
}, },
{ {
"targets": [1], "targets": [1],
"data":"ip_address", "data": "ip_address",
"width": "15%",
"className": "modal-control no-wrap",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData) { if (cellData) {
if (isPrivateIP(cellData)) { if (isPrivateIP(cellData)) {
@@ -46,31 +44,83 @@ user_ip_table_options = {
$(td).html('n/a'); $(td).html('n/a');
} }
}, },
"width": "15%" "width": "15%",
"className": "no-wrap modal-control-ip"
}, },
{ {
"targets": [2], "targets": [2],
"data":"play_count", "data":"platform",
"width": "10%", "createdCell": function (td, cellData, rowData, row, col) {
"className": "hidden-xs" 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], "targets": [3],
"data":"platform", "data":"last_watched",
"width": "15%", "createdCell": function (td, cellData, rowData, row, col) {
"className": "hidden-xs" 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], "targets": [4],
"data":"last_watched", "data":"play_count",
"width": "30%", "searchable": false,
"className": "hidden-sm hidden-xs" "width": "10%"
} }
], ],
"drawCallback": function (settings) { "drawCallback": function (settings) {
// Jump to top of page // Jump to top of page
// $('html,body').scrollTop(0); // $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut(); $('#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) { "preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>"; 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 () { $('#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 tr = $(this).parents('tr');
var row = user_ip_table.row( tr ); var row = user_ip_table.row( tr );
var rowData = row.data(); var rowData = row.data();

View File

@@ -18,7 +18,7 @@ users_list_table_options = {
"columnDefs": [ "columnDefs": [
{ {
"targets": [0], "targets": [0],
"data": "thumb", "data": "user_thumb",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === '') { if (cellData === '') {
$(td).html('<img src="interfaces/default/images/gravatar-default-80x80.png" alt="User Logo"/>'); $(td).html('<img src="interfaces/default/images/gravatar-default-80x80.png" alt="User Logo"/>');
@@ -28,8 +28,8 @@ users_list_table_options = {
}, },
"orderable": false, "orderable": false,
"searchable": false, "searchable": false,
"className": "users-poster-face", "width": "5%",
"width": "40px" "className": "users-poster-face"
}, },
{ {
"targets": [1], "targets": [1],
@@ -45,6 +45,7 @@ users_list_table_options = {
$(td).html(cellData); $(td).html(cellData);
} }
}, },
"width": "15%"
}, },
{ {
"targets": [2], "targets": [2],
@@ -57,24 +58,84 @@ users_list_table_options = {
} }
}, },
"searchable": false, "searchable": false,
"className": "hidden-xs", "width": "15%",
"className": "no-wrap hidden-xs"
}, },
{ {
"targets": [3], "targets": [3],
"data": "ip_address", "data": "ip_address",
"render": function ( data, type, full ) { "createdCell": function (td, cellData, rowData, row, col) {
if (data) { if (cellData) {
return data; if (isPrivateIP(cellData)) {
if (cellData != '') {
$(td).html(cellData);
} else { } else {
return "n/a"; $(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 {
$(td).html('n/a');
} }
}, },
"className": "hidden-xs", "width": "15%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
}, },
{ {
"targets": [4], "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", "data": "plays",
"searchable": false "searchable": false,
"width": "10%"
} }
], ],
@@ -82,9 +143,65 @@ users_list_table_options = {
// Jump to top of page // Jump to top of page
//$('html,body').scrollTop(0); //$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut(); $('#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) { "preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>"; var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
showMsg(msg, false, false, 0) 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,6 +86,7 @@ from plexpy import helpers
$(document).ready(function() { $(document).ready(function() {
LoadPlexPyLogs(); LoadPlexPyLogs();
clearSearchButton('log_table', log_table);
}); });
function LoadPlexPyLogs() { function LoadPlexPyLogs() {
@@ -105,11 +106,13 @@ from plexpy import helpers
$("#plexpy-logs-btn").click(function() { $("#plexpy-logs-btn").click(function() {
$("#clear-logs").show(); $("#clear-logs").show();
LoadPlexPyLogs(); LoadPlexPyLogs();
clearSearchButton('log_table', log_table);
}); });
$("#plex-logs-btn").click(function() { $("#plex-logs-btn").click(function() {
$("#clear-logs").hide(); $("#clear-logs").hide();
LoadPlexLogs(); LoadPlexLogs();
clearSearchButton('plex_log_table', plex_log_table);
}); });
$("#clear-logs").click(function() { $("#clear-logs").click(function() {

View File

@@ -29,29 +29,32 @@ DOCUMENTATION :: END
% for item in data: % for item in data:
<div class="dashboard-recent-media-instance"> <div class="dashboard-recent-media-instance">
<li> <li>
<div class="poster">
% if item['type'] == 'season' or item['type'] == 'movie': % if item['type'] == 'season' or item['type'] == 'movie':
<div class="poster-face">
<a href="info?item_id=${item['rating_key']}"> <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"> <div class="poster">
</a> <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>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season': % if item['type'] == 'season':
<h3>${item['parent_title']} - ${item['title']}</h3> <h3>${item['parent_title']}</h3>
% elif item['type'] == 'album': <h3>(${item['title']})</h3>
<h3>${item['title']}</h3>
% elif item['type'] == 'movie': % elif item['type'] == 'movie':
<h3>${item['title']} (${item['year']})</h3> <h3>${item['title']}</h3>
<h3>(${item['year']})</h3>
% endif % endif
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div> <div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</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> </li>
</div> </div>
<script> <script>

View File

@@ -1,7 +1,7 @@
<%inherit file="base.html"/> <%inherit file="base.html"/>
<%! <%!
import plexpy import plexpy
from plexpy import notifiers from plexpy import notifiers, common, versioncheck
available_notification_agents = notifiers.available_notification_agents() 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> <form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-1"> <div role="tabpanel" class="tab-pane active" id="tabs-1">
% if common.VERSION_NUMBER:
<div class="padded-header"> <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> </div>
% endif
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates <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> <p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
</div> </div>
% if plexpy.CURRENT_VERSION: % if plexpy.CURRENT_VERSION:
<p>Current version: ${plexpy.CURRENT_VERSION}</p> <p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
% endif % endif
<div class="padded-header"> <div class="padded-header">
<h3>Display Settings</h3> <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> <p class="help-block">If you have media indexing enabled on your server, use these on the activity pane.</p>
</div> </div>
<div class="padded-header">
<h3>Homepage Statistics</h3>
</div>
<div class="form-group"> <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="row">
<div class="col-md-2"> <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> <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> </div>
<p class="help-block">Specify the number of days for the statistics on the home page. Default is 30 days.</p> <p class="help-block">Specify the number of days for the statistics on the home page. Default is 30 days.</p>
</div> </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"> <div class="padded-header">
<h3>Plex Logs</h3> <h3>Plex Logs</h3>
@@ -810,10 +822,18 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{season_num}</strong></td> <td width="150"><strong>{season_num}</strong></td>
<td>The season number for the media item if item is episode.</td> <td>The season number for the media item if item is episode.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{season_num00}</strong></td>
<td>The two digit season number.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{episode_num}</strong></td> <td width="150"><strong>{episode_num}</strong></td>
<td>The episode number for the media item if item is episode.</td> <td>The episode number for the media item if item is episode.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{episode_num00}</strong></td>
<td>The two digit episode number.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{rating}</strong></td> <td width="150"><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</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 width="150"><strong>{duration}</strong></td>
<td>The duration (in minutes) for the item.</td> <td>The duration (in minutes) for the item.</td>
</tr> </tr>
<tr>
<td width="150"><strong>{stream_duration}</strong></td>
<td>The stream duration (in minutes) for the item.</td>
</tr>
<tr> <tr>
<td width="150"><strong>{progress}</strong></td> <td width="150"><strong>{progress}</strong></td>
<td>The last reported offset (in minutes) for the item.</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>
</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> </div>
</%def> </%def>
@@ -1023,7 +1062,9 @@ $(document).ready(function() {
headers: {'Content-Type': 'application/xml; charset=utf-8', headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy', 'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': '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']}', 'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val()) 'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
}, },

View File

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

View File

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

View File

@@ -103,7 +103,7 @@ from plexpy import helpers
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-header"> <div class="table-card-header">
<div class="header-bar"> <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> </div>
<div class="table-card-back"> <div class="table-card-back">
@@ -131,17 +131,15 @@ from plexpy import helpers
<table id="user_ip_table" class="display" width="100%"> <table id="user_ip_table" class="display" width="100%">
<thead> <thead>
<tr> <tr>
<th align="left">Last seen</th> <th align="left">Last Seen</th>
<th align="left">IP Address</th> <th align="left">IP Address</th>
<th align="left">Play Count</th> <th align="left">Last Platform</th>
<th align="left">Platform (Last Seen)</th>
<th align="left">Last Watched</th> <th align="left">Last Watched</th>
<th align="left">Play Count</th>
</tr> </tr>
</thead> </thead>
</table> </table>
</div> </div>
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -156,13 +154,17 @@ from plexpy import helpers
<span class="set-username">${data['friendly_name']}</span> <span class="set-username">${data['friendly_name']}</span>
</strong></span> </strong></span>
</div> </div>
<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 class="colvis-button-bar hidden-xs" id="button-bar-history">
</div> </div>
</div> </div>
</div>
<div class="table-card-back"> <div class="table-card-back">
<table class="display" id="history_table" width="100%"> <table class="display" id="history_table" width="100%">
<thead> <thead>
<tr> <tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th> <th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th> <th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</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="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th> <th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th> <th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th> <th align='left' id="percent_complete"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>
</table> </table>
</div> </div>
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal"></div>
</div> </div>
</div> </div>
</div> </div>
@@ -222,6 +223,10 @@ from plexpy import helpers
</div> </div>
</div> </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> </div>
<footer></footer> <footer></footer>
</%def> </%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/user_ips.js"></script>
<script src="interfaces/default/js/tables/sync_table.js"></script> <script src="interfaces/default/js/tables/sync_table.js"></script>
<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 () { $(document).ready(function () {
% if data['user_id']: % 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() { $( "#history-tab-btn" ).one( "click", function() {
// Build watch history table // Build watch history table
history_table_options.ajax = { history_table_options.ajax = {
@@ -289,10 +311,12 @@ from plexpy import helpers
} }
} }
history_table = $('#history_table').DataTable(history_table_options); 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'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table);
}); });
$( "#ip-tab-btn" ).one( "click", function() { $( "#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); user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
clearSearchButton('user_ip_table', user_ip_table);
}); });
$( "#sync-tab-btn" ).one( "click", function() { $( "#sync-tab-btn" ).one( "click", function() {
@@ -320,10 +346,12 @@ from plexpy import helpers
} }
} }
sync_table = $('#sync_table').DataTable(sync_table_options); 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'); $( colvis_sync.button() ).appendTo('#button-bar-sync');
clearSearchButton('sync_table', sync_table);
}); });
// Load edit user modal // 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> </script>
</%def> </%def>

View File

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

View File

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

View File

@@ -23,12 +23,18 @@
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="last_seen">Last Seen</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_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> <th align="left" id="total_plays">Total Plays</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
</tbody> </tbody>
</table> </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>
</div> </div>
@@ -41,6 +47,7 @@
<script src="interfaces/default/js/moment-with-locale.js"></script> <script src="interfaces/default/js/moment-with-locale.js"></script>
<script src="interfaces/default/js/tables/users.js"></script> <script src="interfaces/default/js/tables/users.js"></script>
<script> <script>
$(document).ready(function() {
users_list_table_options.ajax = { users_list_table_options.ajax = {
"url": "get_user_list", "url": "get_user_list",
type: "post", type: "post",
@@ -49,7 +56,11 @@
} }
} }
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() { $("#refresh-users-list").click(function() {
$.ajax({ $.ajax({

View File

@@ -1,6 +1,6 @@
<% <%
import plexpy import plexpy
from plexpy import version from plexpy import common
%> %>
<!doctype html> <!doctype html>
@@ -355,7 +355,9 @@ from plexpy import version
headers: {'Content-Type': 'application/xml; charset=utf-8', headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy', 'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': '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']}', 'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val()) '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 # Service Unit file for systemd system manager
# #
@@ -53,7 +53,7 @@
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode) # graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit] [Unit]
Description=PlexPy - Automatic music downloader for SABnzbd Description=PlexPy
[Service] [Service]
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet 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 # 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 # 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 @author: Michael
''' '''
import platform import platform
import operator
import os
import re
from plexpy import version from plexpy import version
# Identify Our Application # 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 # Notification Types
NOTIFY_STARTED = 1 NOTIFY_STARTED = 1

View File

@@ -83,6 +83,7 @@ _CONFIG_DEFINITIONS = {
'GROWL_ON_BUFFER': (int, 'Growl', 0), 'GROWL_ON_BUFFER': (int, 'Growl', 0),
'GROWL_ON_WATCHED': (int, 'Growl', 0), 'GROWL_ON_WATCHED': (int, 'Growl', 0),
'HOME_STATS_LENGTH': (int, 'General', 30), 'HOME_STATS_LENGTH': (int, 'General', 30),
'HOME_STATS_TYPE': (int, 'General', 0),
'HTTPS_CERT': (str, 'General', ''), 'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''), 'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'), '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 # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -26,63 +26,6 @@ class DataFactory(object):
def __init__(self): def __init__(self):
pass pass
def get_user_list(self, kwargs=None):
data_tables = datatables.DataTables()
columns = ['users.user_id as user_id',
'users.custom_avatar_url as 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',
'users.username as user'
]
try:
query = data_tables.ssp_query(table_name='users',
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']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
users = query['result']
rows = []
for item in users:
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
row = {"plays": item['plays'],
"last_seen": item['last_seen'],
"friendly_name": item["friendly_name"],
"ip_address": item["ip_address"],
"thumb": user_thumb,
"user": item["user"],
"user_id": item['user_id']
}
rows.append(row)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
def get_history(self, kwargs=None, custom_where=None): def get_history(self, kwargs=None, custom_where=None):
data_tables = datatables.DataTables() data_tables = datatables.DataTables()
@@ -93,6 +36,13 @@ class DataFactory(object):
'session_history.player', 'session_history.player',
'session_history.ip_address', 'session_history.ip_address',
'session_history_metadata.full_title as full_title', '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.started',
'session_history.paused_counter', 'session_history.paused_counter',
'session_history.stopped', 'session_history.stopped',
@@ -138,12 +88,24 @@ class DataFactory(object):
rows = [] rows = []
for item in history: 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'], row = {"id": item['id'],
"date": item['date'], "date": item['date'],
"friendly_name": item['friendly_name'], "friendly_name": item['friendly_name'],
"player": item["player"], "player": item["player"],
"ip_address": item["ip_address"], "ip_address": item["ip_address"],
"full_title": item["full_title"], "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"], "started": item["started"],
"paused_counter": item["paused_counter"], "paused_counter": item["paused_counter"],
"stopped": item["stopped"], "stopped": item["stopped"],
@@ -167,313 +129,16 @@ class DataFactory(object):
return dict return dict
def get_user_unique_ips(self, kwargs=None, custom_where=None): def get_home_stats(self, time_range='30', stat_type='0'):
data_tables = datatables.DataTables()
columns = ['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.user as user',
'session_history.user_id as user_id'
]
try:
query = data_tables.ssp_query(table_name='session_history',
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']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
results = query['result']
rows = []
for item in results:
row = {"last_seen": item['last_seen'],
"ip_address": item['ip_address'],
"play_count": item['play_count'],
"platform": item['platform'],
"last_watched": item['last_watched']
}
rows.append(row)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
# TODO: The getter and setter for this needs to become a config getter/setter for more than just friendlyname
def set_user_friendly_name(self, user=None, user_id=None, friendly_name=None, do_notify=0, keep_history=1):
if user_id:
if friendly_name.strip() == '':
friendly_name = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"user_id": user_id}
new_value_dict = {"friendly_name": friendly_name,
"do_notify": do_notify,
"keep_history": keep_history}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
if user:
if friendly_name.strip() == '':
friendly_name = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"username": user}
new_value_dict = {"friendly_name": friendly_name,
"do_notify": do_notify,
"keep_history": keep_history}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
def set_user_profile_url(self, user=None, user_id=None, profile_url=None):
if user_id:
if profile_url.strip() == '':
profile_url = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"user_id": user_id}
new_value_dict = {"custom_avatar_url": profile_url}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
if user:
if profile_url.strip() == '':
profile_url = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"username": user}
new_value_dict = {"custom_avatar_url": profile_url}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
def get_user_friendly_name(self, user=None, user_id=None):
if user_id:
monitor_db = database.MonitorDatabase()
query = 'select username, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'do_notify, keep_history, custom_avatar_url as thumb ' \
'FROM users WHERE user_id = ?'
result = monitor_db.select(query, args=[user_id])
if result:
user_detail = {'user_id': user_id,
'user': result[0][0],
'friendly_name': result[0][1],
'thumb': result[0][4],
'do_notify': helpers.checked(result[0][2]),
'keep_history': helpers.checked(result[0][3])
}
return user_detail
else:
user_detail = {'user_id': user_id,
'user': '',
'friendly_name': '',
'do_notify': '',
'thumb': '',
'keep_history': ''}
return user_detail
elif user:
monitor_db = database.MonitorDatabase()
query = 'select user_id, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'do_notify, keep_history, custom_avatar_url as thumb ' \
'FROM users WHERE username = ?'
result = monitor_db.select(query, args=[user])
if result:
user_detail = {'user_id': result[0][0],
'user': user,
'friendly_name': result[0][1],
'thumb': result[0][4],
'do_notify': helpers.checked(result[0][2]),
'keep_history': helpers.checked(result[0][3])}
return user_detail
else:
user_detail = {'user_id': None,
'user': user,
'friendly_name': '',
'do_notify': '',
'thumb': '',
'keep_history': ''}
return user_detail
return None
def get_user_id(self, user=None):
if user:
try:
monitor_db = database.MonitorDatabase()
query = 'select user_id FROM users WHERE username = ?'
result = monitor_db.select_single(query, args=[user])
if result:
return result
else:
return None
except:
return None
return None
def get_user_details(self, user=None, user_id=None):
from plexpy import plextv
monitor_db = database.MonitorDatabase()
if user:
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else:
result = None
if result:
user_details = {}
for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'],
"username": item['username'],
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
}
return user_details
else:
logger.warn(u"PlexPy :: Unable to retrieve user from local database. Requesting user list refresh.")
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
if user:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else:
result = None
if result:
user_details = {}
for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'],
"username": item['username'],
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
}
return user_details
else:
# If there is no user data we must return something
# Use "Local" user to retain compatibility with PlexWatch database value
return {"user_id": None,
"username": 'Local',
"friendly_name": 'Local',
"email": '',
"thumb": '',
"is_home_user": 0,
"is_allow_sync": 0,
"is_restricted": 0,
"do_notify": 0
}
def get_home_stats(self, time_range='30'):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
if not time_range.isdigit(): if not time_range.isdigit():
time_range = '30' time_range = '30'
sort_type = 'total_plays' if stat_type == '0' else 'total_duration'
# This actually determines the output order in the home page # 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 = [] home_stats = []
for stat in stats_queries: for stat in stats_queries:
@@ -483,6 +148,10 @@ class DataFactory(object):
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.grandparent_title, ' \ 'session_history_metadata.grandparent_title, ' \
'COUNT(session_history_metadata.grandparent_title) as total_plays, ' \ '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, ' \ 'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch,' \ 'MAX(session_history.started) as last_watch,' \
'session_history_metadata.grandparent_thumb ' \ 'session_history_metadata.grandparent_thumb ' \
@@ -492,7 +161,7 @@ class DataFactory(object):
'>= datetime("now", "-%s days", "localtime") ' \ '>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "episode" ' \ 'AND session_history_metadata.media_type = "episode" ' \
'GROUP BY session_history_metadata.grandparent_title ' \ '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) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query.")
@@ -501,10 +170,11 @@ class DataFactory(object):
for item in result: for item in result:
row = {'title': item[1], row = {'title': item[1],
'total_plays': item[2], 'total_plays': item[2],
'total_duration': item[3],
'users_watched': '', 'users_watched': '',
'rating_key': item[3], 'rating_key': item[4],
'last_play': item[4], 'last_play': item[5],
'grandparent_thumb': item[5], 'grandparent_thumb': item[6],
'thumb': '', 'thumb': '',
'user': '', 'user': '',
'friendly_name': '', 'friendly_name': '',
@@ -515,6 +185,7 @@ class DataFactory(object):
top_tv.append(row) top_tv.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_tv}) 'rows': top_tv})
elif 'top_movies' in stat: elif 'top_movies' in stat:
@@ -523,6 +194,10 @@ class DataFactory(object):
query = 'SELECT session_history_metadata.id, ' \ query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \ 'session_history_metadata.full_title, ' \
'COUNT(session_history_metadata.full_title) as total_plays, ' \ '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, ' \ 'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch,' \ 'MAX(session_history.started) as last_watch,' \
'session_history_metadata.thumb ' \ 'session_history_metadata.thumb ' \
@@ -532,7 +207,7 @@ class DataFactory(object):
'>= datetime("now", "-%s days", "localtime") ' \ '>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "movie" ' \ 'AND session_history_metadata.media_type = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \ '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) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query.")
@@ -541,11 +216,12 @@ class DataFactory(object):
for item in result: for item in result:
row = {'title': item[1], row = {'title': item[1],
'total_plays': item[2], 'total_plays': item[2],
'total_duration': item[3],
'users_watched': '', 'users_watched': '',
'rating_key': item[3], 'rating_key': item[4],
'last_play': item[4], 'last_play': item[5],
'grandparent_thumb': '', 'grandparent_thumb': '',
'thumb': item[5], 'thumb': item[6],
'user': '', 'user': '',
'friendly_name': '', 'friendly_name': '',
'platform_type': '', 'platform_type': '',
@@ -555,6 +231,7 @@ class DataFactory(object):
top_movies.append(row) top_movies.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_movies}) 'rows': top_movies})
elif 'popular_tv' in stat: elif 'popular_tv' in stat:
@@ -599,6 +276,48 @@ class DataFactory(object):
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'rows': popular_tv}) '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: elif 'top_users' in stat:
top_users = [] top_users = []
try: try:
@@ -606,6 +325,10 @@ class DataFactory(object):
'(case when users.friendly_name is null then session_history.user else ' \ '(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \ 'users.friendly_name end) as friendly_name,' \
'COUNT(session_history.id) as total_plays, ' \ '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, ' \ 'MAX(session_history.started) as last_watch, ' \
'users.custom_avatar_url as thumb, ' \ 'users.custom_avatar_url as thumb, ' \
'users.user_id ' \ 'users.user_id ' \
@@ -615,23 +338,24 @@ class DataFactory(object):
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \ 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") '\ 'datetime("now", "-%s days", "localtime") '\
'GROUP BY session_history.user_id ' \ '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) result = monitor_db.select(query)
except: except:
logger.warn("Unable to execute database query.") logger.warn("Unable to execute database query.")
return None return None
for item in result: for item in result:
if not item[4] or item[4] == '': if not item[5] or item[5] == '':
user_thumb = common.DEFAULT_USER_THUMB user_thumb = common.DEFAULT_USER_THUMB
else: else:
user_thumb = item[4] user_thumb = item[5]
row = {'user': item[0], row = {'user': item[0],
'user_id': item[5], 'user_id': item[6],
'friendly_name': item[1], 'friendly_name': item[1],
'total_plays': item[2], 'total_plays': item[2],
'last_play': item[3], 'total_duration': item[3],
'last_play': item[4],
'thumb': user_thumb, 'thumb': user_thumb,
'grandparent_thumb': '', 'grandparent_thumb': '',
'users_watched': '', 'users_watched': '',
@@ -644,6 +368,7 @@ class DataFactory(object):
top_users.append(row) top_users.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_users}) 'rows': top_users})
elif 'top_platforms' in stat: elif 'top_platforms' in stat:
@@ -652,6 +377,10 @@ class DataFactory(object):
try: try:
query = 'SELECT session_history.platform, ' \ query = 'SELECT session_history.platform, ' \
'COUNT(session_history.id) as total_plays, ' \ '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 ' \ 'MAX(session_history.started) as last_watch ' \
'FROM session_history ' \ 'FROM session_history ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \ 'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
@@ -666,7 +395,8 @@ class DataFactory(object):
for item in result: for item in result:
row = {'platform': item[0], row = {'platform': item[0],
'total_plays': item[1], 'total_plays': item[1],
'last_play': item[2], 'total_duration': item[2],
'last_play': item[3],
'platform_type': item[0], 'platform_type': item[0],
'title': '', 'title': '',
'thumb': '', 'thumb': '',
@@ -680,6 +410,7 @@ class DataFactory(object):
top_platform.append(row) top_platform.append(row)
home_stats.append({'stat_id': stat, home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_platform}) 'rows': top_platform})
return home_stats return home_stats
@@ -699,7 +430,6 @@ class DataFactory(object):
else: else:
return None return None
print result
stream_output = {} stream_output = {}
for item in result: for item in result:
@@ -737,21 +467,21 @@ class DataFactory(object):
try: try:
if user_id: if user_id:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \ 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 ' \ 'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ '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 ?' 'WHERE user_id = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user_id, limit]) result = monitor_db.select(query, args=[user_id, limit])
elif user: elif user:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \ 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 ' \ 'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?' 'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user, limit]) result = monitor_db.select(query, args=[user, limit])
else: else:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \ 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"' \ 'FROM session_history_metadata WHERE session_history.media_type != "track"' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \ 'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'ORDER BY started DESC LIMIT ?' 'ORDER BY started DESC LIMIT ?'
@@ -761,111 +491,29 @@ class DataFactory(object):
return None return None
for row in result: for row in result:
if row[1] == 'episode': if row[1] == 'episode' and row[6]:
thumb = row[5] thumb = row[6]
elif row[1] == 'episode':
thumb = row[7]
else: else:
thumb = row[4] thumb = row[5]
recent_output = {'row_id': row[0], recent_output = {'row_id': row[0],
'type': row[1], 'type': row[1],
'rating_key': row[2], 'rating_key': row[2],
'title': row[3], 'title': row[3],
'parent_title': row[4],
'thumb': thumb, 'thumb': thumb,
'index': row[6], 'index': row[8],
'parentIndex': row[7], 'parent_index': row[9],
'year': row[8], 'year': row[10],
'time': row[9], 'time': row[11],
'user': row[10] 'user': row[12]
} }
recently_watched.append(recent_output) recently_watched.append(recent_output)
return recently_watched return recently_watched
def get_user_watch_time_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase()
time_queries = [1, 7, 30, 0]
user_watch_time_stats = []
for days in time_queries:
if days > 0:
if user_id:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user_id = ?' % days
result = monitor_db.select(query, args=[user_id])
elif user:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user = ?' % days
result = monitor_db.select(query, args=[user])
else:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE user = ?'
result = monitor_db.select(query, args=[user])
for item in result:
if item[0]:
total_time = item[0]
total_plays = item[1]
else:
total_time = 0
total_plays = 0
row = {'query_days': days,
'total_time': total_time,
'total_plays': total_plays
}
user_watch_time_stats.append(row)
return user_watch_time_stats
def get_user_platform_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase()
platform_stats = []
result_id = 0
try:
if user_id:
query = 'SELECT player, COUNT(player) as player_count, platform ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user_id])
else:
query = 'SELECT player, COUNT(player) as player_count, platform ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user])
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
row = {'platform_name': item[0],
'platform_type': item[2],
'total_plays': item[1],
'result_id': result_id
}
platform_stats.append(row)
result_id += 1
return platform_stats
def get_metadata_details(self, row_id): def get_metadata_details(self, row_id):
monitor_db = database.MonitorDatabase() monitor_db = database.MonitorDatabase()
@@ -882,13 +530,15 @@ class DataFactory(object):
metadata = {} metadata = {}
for item in result: for item in result:
directors = item['directors'].split(';') directors = item['directors'].split(';') if item['directors'] else []
writers = item['writers'].split(';') writers = item['writers'].split(';') if item['writers'] else []
actors = item['actors'].split(';') actors = item['actors'].split(';') if item['actors'] else []
genres = item['genres'].split(';') genres = item['genres'].split(';') if item['genres'] else []
metadata = {'type': item['media_type'], metadata = {'type': item['media_type'],
'rating_key': item['rating_key'], 'rating_key': item['rating_key'],
'parent_rating_key': item['parent_rating_key'],
'grandparent_rating_key': item['grandparent_rating_key'],
'grandparent_title': item['grandparent_title'], 'grandparent_title': item['grandparent_title'],
'parent_index': item['parent_media_index'], 'parent_index': item['parent_media_index'],
'parent_title': item['parent_title'], 'parent_title': item['parent_title'],
@@ -916,3 +566,47 @@ class DataFactory(object):
} }
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

@@ -231,10 +231,10 @@ class MonitorProcessing(object):
self.db.upsert('sessions', timestamp, keys) self.db.upsert('sessions', timestamp, keys)
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0): def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
from plexpy import datafactory from plexpy import users
data_factory = datafactory.DataFactory() user_data = users.Users()
user_details = data_factory.get_user_friendly_name(user=session['user']) user_details = user_data.get_user_friendly_name(user=session['user'])
if session: if session:
logging_enabled = False logging_enabled = False

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 # PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by # it under the terms of the GNU General Public License as published by
@@ -20,12 +20,12 @@ import time
def notify(stream_data=None, notify_action=None): def notify(stream_data=None, notify_action=None):
from plexpy import datafactory from plexpy import users
if stream_data and notify_action: if stream_data and notify_action:
# Check if notifications enabled for user # Check if notifications enabled for user
data_factory = datafactory.DataFactory() user_data = users.Users()
user_details = data_factory.get_user_friendly_name(user=stream_data['user']) user_details = user_data.get_user_friendly_name(user=stream_data['user'])
if not user_details['do_notify']: if not user_details['do_notify']:
return return
@@ -300,6 +300,7 @@ def build_notify_text(session, state):
duration = helpers.convert_milliseconds_to_minutes(item_metadata['duration']) duration = helpers.convert_milliseconds_to_minutes(item_metadata['duration'])
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset']) 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) progress_percent = helpers.get_percent(view_offset, duration)
@@ -317,10 +318,13 @@ def build_notify_text(session, state):
'content_rating': item_metadata['content_rating'], 'content_rating': item_metadata['content_rating'],
'summary': item_metadata['summary'], 'summary': item_metadata['summary'],
'season_num': item_metadata['parent_index'], 'season_num': item_metadata['parent_index'],
'season_num00': item_metadata['parent_index'].zfill(2),
'episode_num': item_metadata['index'], 'episode_num': item_metadata['index'],
'episode_num00': item_metadata['index'].zfill(2),
'album_name': item_metadata['parent_title'], 'album_name': item_metadata['parent_title'],
'rating': item_metadata['rating'], 'rating': item_metadata['rating'],
'duration': duration, 'duration': duration,
'stream_duration': stream_duration,
'progress': view_offset, 'progress': view_offset,
'progress_percent': progress_percent 'progress_percent': progress_percent
} }

View File

@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers, datafactory, http_handler, database from plexpy import logger, helpers, users, http_handler, database
from xml.dom import minidom from xml.dom import minidom
@@ -38,13 +38,16 @@ def refresh_users():
} }
# Check if we've set a custom avatar if so don't overwrite it. # Check if we've set a custom avatar if so don't overwrite it.
if item['user_id']:
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url ' avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
'FROM users WHERE user_id = ?', 'FROM users WHERE user_id = ?',
[item['user_id']]) [item['user_id']])
if avatar_urls:
if not avatar_urls[0]['custom_avatar_url'] or \ if not avatar_urls[0]['custom_avatar_url'] or \
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']: avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
new_value_dict['custom_avatar_url'] = item['thumb'] new_value_dict['custom_avatar_url'] = item['thumb']
else:
new_value_dict['custom_avatar_url'] = item['thumb']
monitor_db.upsert('users', new_value_dict, control_value_dict) monitor_db.upsert('users', new_value_dict, control_value_dict)
@@ -253,7 +256,7 @@ class PlexTV(object):
def get_synced_items(self, machine_id=None, user_id=None): def get_synced_items(self, machine_id=None, user_id=None):
sync_list = self.get_plextv_sync_lists(machine_id) sync_list = self.get_plextv_sync_lists(machine_id)
data_factory = datafactory.DataFactory() user_data = users.Users()
synced_items = [] synced_items = []
@@ -277,8 +280,8 @@ class PlexTV(object):
for device in sync_device: for device in sync_device:
device_user_id = helpers.get_xml_attr(device, 'userID') device_user_id = helpers.get_xml_attr(device, 'userID')
try: try:
device_username = data_factory.get_user_details(user_id=device_user_id)['username'] device_username = user_data.get_user_details(user_id=device_user_id)['username']
device_friendly_name = data_factory.get_user_details(user_id=device_user_id)['friendly_name'] device_friendly_name = user_data.get_user_details(user_id=device_user_id)['friendly_name']
except: except:
device_username = '' device_username = ''
device_friendly_name = '' device_friendly_name = ''

View File

@@ -15,7 +15,7 @@
import sqlite3 import sqlite3
from plexpy import logger, helpers, monitor, datafactory, plextv from plexpy import logger, helpers, monitor, users, plextv
from xml.dom import minidom from xml.dom import minidom
import plexpy import plexpy
@@ -246,7 +246,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
plexpy.schedule_job(monitor.check_active_sessions, 'Check for active sessions', hours=0, minutes=0, seconds=0) plexpy.schedule_job(monitor.check_active_sessions, 'Check for active sessions', hours=0, minutes=0, seconds=0)
monitor_processing = monitor.MonitorProcessing() monitor_processing = monitor.MonitorProcessing()
data_factory = datafactory.DataFactory() user_data = users.Users()
# Get the latest friends list so we can pull user id's # Get the latest friends list so we can pull user id's
try: try:
@@ -292,8 +292,8 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
continue continue
# If the user_id no longer exists in the friends list, pull it from the xml. # If the user_id no longer exists in the friends list, pull it from the xml.
if data_factory.get_user_id(user=row['user']): if user_data.get_user_id(user=row['user']):
user_id = data_factory.get_user_id(user=row['user']) user_id = user_data.get_user_id(user=row['user'])
else: else:
user_id = extracted_xml['user_id'] user_id = extracted_xml['user_id']

View File

@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers, datafactory, http_handler from plexpy import logger, helpers, users, http_handler
from urlparse import urlparse from urlparse import urlparse
import plexpy import plexpy
@@ -507,7 +507,7 @@ class PmsConnect(object):
""" """
def get_session_each(self, stream_type='', session=None): def get_session_each(self, stream_type='', session=None):
session_output = None session_output = None
data_factory = datafactory.DataFactory() user_data = users.Users()
if stream_type == 'track': if stream_type == 'track':
media_info = session.getElementsByTagName('Media')[0] media_info = session.getElementsByTagName('Media')[0]
@@ -533,7 +533,7 @@ class PmsConnect(object):
transcode_container = '' transcode_container = ''
transcode_protocol = '' transcode_protocol = ''
user_details = data_factory.get_user_details( user_details = user_data.get_user_details(
user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title')) user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title'))
if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Track'): if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Track'):
@@ -641,7 +641,7 @@ class PmsConnect(object):
else: else:
use_indexes = 0 use_indexes = 0
user_details = data_factory.get_user_details( user_details = user_data.get_user_details(
user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title')) user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title'))
if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Video'): if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Video'):

523
plexpy/users.py Normal file
View File

@@ -0,0 +1,523 @@
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# PlexPy is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, datatables, common, database, helpers
class Users(object):
def __init__(self):
pass
def get_user_list(self, kwargs=None):
data_tables = datatables.DataTables()
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:
query = data_tables.ssp_query(table_name='users',
columns=columns,
custom_where=[],
group_by=['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.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
users = query['result']
rows = []
for item in users:
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['user_thumb']
row = {"id": item['id'],
"plays": item['plays'],
"last_seen": item['last_seen'],
"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']
}
rows.append(row)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
def get_user_unique_ips(self, kwargs=None, custom_where=None):
data_tables = datatables.DataTables()
# 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 custom_user_id',
'(case when users.friendly_name is null then users.username else \
users.friendly_name end) as friendly_name'
]
try:
query = data_tables.ssp_query(table_name='session_history',
columns=columns,
custom_where=custom_where,
group_by=['ip_address'],
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.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
results = query['result']
rows = []
for item in results:
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'],
"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)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
# TODO: The getter and setter for this needs to become a config getter/setter for more than just friendlyname
def set_user_friendly_name(self, user=None, user_id=None, friendly_name=None, do_notify=0, keep_history=1):
if user_id:
if friendly_name.strip() == '':
friendly_name = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"user_id": user_id}
new_value_dict = {"friendly_name": friendly_name,
"do_notify": do_notify,
"keep_history": keep_history}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
if user:
if friendly_name.strip() == '':
friendly_name = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"username": user}
new_value_dict = {"friendly_name": friendly_name,
"do_notify": do_notify,
"keep_history": keep_history}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
def set_user_profile_url(self, user=None, user_id=None, profile_url=None):
if user_id:
if profile_url.strip() == '':
profile_url = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"user_id": user_id}
new_value_dict = {"custom_avatar_url": profile_url}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
if user:
if profile_url.strip() == '':
profile_url = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"username": user}
new_value_dict = {"custom_avatar_url": profile_url}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
def get_user_friendly_name(self, user=None, user_id=None):
if user_id:
monitor_db = database.MonitorDatabase()
query = 'select username, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'do_notify, keep_history, custom_avatar_url as thumb ' \
'FROM users WHERE user_id = ?'
result = monitor_db.select(query, args=[user_id])
if result:
user_detail = {'user_id': user_id,
'user': result[0][0],
'friendly_name': result[0][1],
'thumb': result[0][4],
'do_notify': helpers.checked(result[0][2]),
'keep_history': helpers.checked(result[0][3])
}
return user_detail
else:
user_detail = {'user_id': user_id,
'user': '',
'friendly_name': '',
'do_notify': '',
'thumb': '',
'keep_history': ''}
return user_detail
elif user:
monitor_db = database.MonitorDatabase()
query = 'select user_id, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'do_notify, keep_history, custom_avatar_url as thumb ' \
'FROM users WHERE username = ?'
result = monitor_db.select(query, args=[user])
if result:
user_detail = {'user_id': result[0][0],
'user': user,
'friendly_name': result[0][1],
'thumb': result[0][4],
'do_notify': helpers.checked(result[0][2]),
'keep_history': helpers.checked(result[0][3])}
return user_detail
else:
user_detail = {'user_id': None,
'user': user,
'friendly_name': '',
'do_notify': '',
'thumb': '',
'keep_history': ''}
return user_detail
return None
def get_user_id(self, user=None):
if user:
try:
monitor_db = database.MonitorDatabase()
query = 'select user_id FROM users WHERE username = ?'
result = monitor_db.select_single(query, args=[user])
if result:
return result
else:
return None
except:
return None
return None
def get_user_details(self, user=None, user_id=None):
from plexpy import plextv
monitor_db = database.MonitorDatabase()
if user:
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else:
result = None
if result:
user_details = {}
for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'],
"username": item['username'],
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
}
return user_details
else:
logger.warn(u"PlexPy :: Unable to retrieve user from local database. Requesting user list refresh.")
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
if user:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'custom_avatar_url as thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else:
result = None
if result:
user_details = {}
for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'],
"username": item['username'],
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
}
return user_details
else:
# If there is no user data we must return something
# Use "Local" user to retain compatibility with PlexWatch database value
return {"user_id": None,
"username": 'Local',
"friendly_name": 'Local',
"email": '',
"thumb": '',
"is_home_user": 0,
"is_allow_sync": 0,
"is_restricted": 0,
"do_notify": 0
}
def get_user_watch_time_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase()
time_queries = [1, 7, 30, 0]
user_watch_time_stats = []
for days in time_queries:
if days > 0:
if user_id:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user_id = ?' % days
result = monitor_db.select(query, args=[user_id])
elif user:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user = ?' % days
result = monitor_db.select(query, args=[user])
else:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE user = ?'
result = monitor_db.select(query, args=[user])
for item in result:
if item[0]:
total_time = item[0]
total_plays = item[1]
else:
total_time = 0
total_plays = 0
row = {'query_days': days,
'total_time': total_time,
'total_plays': total_plays
}
user_watch_time_stats.append(row)
return user_watch_time_stats
def get_user_platform_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase()
platform_stats = []
result_id = 0
try:
if user_id:
query = 'SELECT player, COUNT(player) as player_count, platform ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user_id])
else:
query = 'SELECT player, COUNT(player) as player_count, platform ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user])
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
row = {'platform_name': item[0],
'platform_type': item[2],
'total_plays': item[1],
'result_id': result_id
}
platform_stats.append(row)
result_id += 1
return platform_stats

View File

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

View File

@@ -241,3 +241,37 @@ def update():
e e
) )
return 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

@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>. # along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs, users
from plexpy.helpers import checked, radio from plexpy.helpers import checked, radio
from mako.lookup import TemplateLookup from mako.lookup import TemplateLookup
@@ -65,7 +65,8 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def home(self): def home(self):
config = { 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) return serve_template(templatename="index.html", title="Home", config=config)
@@ -118,9 +119,9 @@ class WebInterface(object):
return json.dumps(formats) return json.dumps(formats)
@cherrypy.expose @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() 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) return serve_template(templatename="home_stats.html", title="Stats", data=stats_data)
@@ -142,16 +143,15 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def user(self, user=None, user_id=None): def user(self, user=None, user_id=None):
user_data = users.Users()
if user_id: if user_id:
try: try:
data_factory = datafactory.DataFactory() user_details = user_data.get_user_details(user_id=user_id)
user_details = data_factory.get_user_details(user_id=user_id)
except: except:
logger.warn("Unable to retrieve friendly name for user_id %s " % user_id) logger.warn("Unable to retrieve friendly name for user_id %s " % user_id)
elif user: elif user:
try: try:
data_factory = datafactory.DataFactory() user_details = user_data.get_user_details(user=user)
user_details = data_factory.get_user_details(user=user)
except: except:
logger.warn("Unable to retrieve friendly name for user %s " % user) logger.warn("Unable to retrieve friendly name for user %s " % user)
else: else:
@@ -162,13 +162,12 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def edit_user_dialog(self, user=None, user_id=None, **kwargs): def edit_user_dialog(self, user=None, user_id=None, **kwargs):
user_data = users.Users()
if user_id: if user_id:
data_factory = datafactory.DataFactory() result = user_data.get_user_friendly_name(user_id=user_id)
result = data_factory.get_user_friendly_name(user_id=user_id)
status_message = '' status_message = ''
elif user: elif user:
data_factory = datafactory.DataFactory() result = user_data.get_user_friendly_name(user=user)
result = data_factory.get_user_friendly_name(user=user)
status_message = '' status_message = ''
else: else:
result = None result = None
@@ -191,14 +190,14 @@ class WebInterface(object):
else: else:
custom_avatar = '' custom_avatar = ''
user_data = users.Users()
if user_id: if user_id:
try: try:
data_factory = datafactory.DataFactory() user_data.set_user_friendly_name(user_id=user_id,
data_factory.set_user_friendly_name(user_id=user_id,
friendly_name=friendly_name, friendly_name=friendly_name,
do_notify=do_notify, do_notify=do_notify,
keep_history=keep_history) keep_history=keep_history)
data_factory.set_user_profile_url(user_id=user_id, user_data.set_user_profile_url(user_id=user_id,
profile_url=custom_avatar) profile_url=custom_avatar)
status_message = "Successfully updated user." status_message = "Successfully updated user."
@@ -208,12 +207,11 @@ class WebInterface(object):
return status_message return status_message
if user: if user:
try: try:
data_factory = datafactory.DataFactory() user_data.set_user_friendly_name(user=user,
data_factory.set_user_friendly_name(user=user,
friendly_name=friendly_name, friendly_name=friendly_name,
do_notify=do_notify, do_notify=do_notify,
keep_history=keep_history) keep_history=keep_history)
data_factory.set_user_profile_url(user=user, user_data.set_user_profile_url(user=user,
profile_url=custom_avatar) profile_url=custom_avatar)
status_message = "Successfully updated user." status_message = "Successfully updated user."
@@ -244,11 +242,11 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def get_user_list(self, **kwargs): def get_user_list(self, **kwargs):
data_factory = datafactory.DataFactory() user_data = users.Users()
users = data_factory.get_user_list(kwargs=kwargs) user_list = user_data.get_user_list(kwargs=kwargs)
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps(users) return json.dumps(user_list)
@cherrypy.expose @cherrypy.expose
def checkGithub(self): def checkGithub(self):
@@ -454,6 +452,7 @@ class WebInterface(object):
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT, "notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT, "notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH, "home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE),
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD, "buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT "buffer_wait": plexpy.CONFIG.BUFFER_WAIT
} }
@@ -476,7 +475,7 @@ class WebInterface(object):
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start", "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_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", "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: for checked_config in checked_configs:
if checked_config not in kwargs: if checked_config not in kwargs:
@@ -763,8 +762,8 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs): def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs):
data_factory = datafactory.DataFactory() user_data = users.Users()
result = data_factory.get_user_watch_time_stats(user_id=user_id, user=user) result = user_data.get_user_watch_time_stats(user_id=user_id, user=user)
if result: if result:
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats") return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
@@ -775,8 +774,8 @@ class WebInterface(object):
@cherrypy.expose @cherrypy.expose
def get_user_platform_stats(self, user=None, user_id=None, **kwargs): def get_user_platform_stats(self, user=None, user_id=None, **kwargs):
data_factory = datafactory.DataFactory() user_data = users.Users()
result = data_factory.get_user_platform_stats(user_id=user_id, user=user) result = user_data.get_user_platform_stats(user_id=user_id, user=user)
if result: if result:
return serve_template(templatename="user_platform_stats.html", data=result, return serve_template(templatename="user_platform_stats.html", data=result,
@@ -854,8 +853,8 @@ class WebInterface(object):
elif user: elif user:
custom_where = [['user', user]] custom_where = [['user', user]]
data_factory = datafactory.DataFactory() user_data = users.Users()
history = data_factory.get_user_unique_ips(kwargs=kwargs, history = user_data.get_user_unique_ips(kwargs=kwargs,
custom_where=custom_where) custom_where=custom_where)
cherrypy.response.headers['Content-type'] = 'application/json' cherrypy.response.headers['Content-type'] = 'application/json'
@@ -1259,3 +1258,32 @@ class WebInterface(object):
return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers", return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers",
data=this_agent) 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'})