Compare commits
66 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
0f04a6d25b | ||
![]() |
2d19accdd1 | ||
![]() |
3d5706002d | ||
![]() |
abec036cb2 | ||
![]() |
8bdd40f011 | ||
![]() |
b98c17e738 | ||
![]() |
a71c6e6592 | ||
![]() |
545b7cb581 | ||
![]() |
0848e317ee | ||
![]() |
e976e6cf5c | ||
![]() |
95cd2b4f81 | ||
![]() |
3df73dc287 | ||
![]() |
3a703eb605 | ||
![]() |
e94c00ca48 | ||
![]() |
d6a34b3e6b | ||
![]() |
3cbf05d54a | ||
![]() |
cba8608c23 | ||
![]() |
e34865d0dd | ||
![]() |
b2a7f639bb | ||
![]() |
fcbc921470 | ||
![]() |
5168d76e86 | ||
![]() |
968d213b97 | ||
![]() |
9fc4573c42 | ||
![]() |
9adf5cc39a | ||
![]() |
2ca04f4a8b | ||
![]() |
fd3b2a48f9 | ||
![]() |
01b3ae377b | ||
![]() |
5a1516286c | ||
![]() |
317a9f0b8e | ||
![]() |
c98505038a | ||
![]() |
1ec1edefdd | ||
![]() |
aa351bd965 | ||
![]() |
519ff6b203 | ||
![]() |
5b2beb2b81 | ||
![]() |
13e6a70a30 | ||
![]() |
7e99eb7a2a | ||
![]() |
6efaabb630 | ||
![]() |
2536fdf17b | ||
![]() |
7e8a427107 | ||
![]() |
bbaf428fd8 | ||
![]() |
7dfd063138 | ||
![]() |
5c94b21bd1 | ||
![]() |
58474d9565 | ||
![]() |
6cb1c057cf | ||
![]() |
22cc06dec3 | ||
![]() |
357797df6b | ||
![]() |
4c6f6ca736 | ||
![]() |
c0214f1489 | ||
![]() |
dd27f9bf72 | ||
![]() |
c1c7911d08 | ||
![]() |
755e9107fa | ||
![]() |
3bb6320fc1 | ||
![]() |
b5ad88ae5a | ||
![]() |
bbcf3bf7da | ||
![]() |
51e1949538 | ||
![]() |
6b1a57e650 | ||
![]() |
8e57df53fd | ||
![]() |
cea4992331 | ||
![]() |
c98a8865d6 | ||
![]() |
fd3daae491 | ||
![]() |
697d107952 | ||
![]() |
65e42be278 | ||
![]() |
ad79d860db | ||
![]() |
5826a823a8 | ||
![]() |
cbec1e7768 | ||
![]() |
40f72bbe5f |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,13 +1,34 @@
|
||||
# Changelog
|
||||
|
||||
## v1.0 (2015-08-11)
|
||||
## v1.1.3 (2015-08-22)
|
||||
|
||||
* First release
|
||||
* Show human readable version info and this cool changelog in Settings -> General.
|
||||
* Add a "delete" mode to the history tables. Toggle it to show a delete button next to each history item.
|
||||
* Two digit season and episode numbers for custom notification messages. Thanks @JohnnyWong.
|
||||
* New FreeNAS init script. Thanks @JohnnyWong.
|
||||
* Lots of styling improvements! Thanks @JohnnyWong.
|
||||
* Graph page remembers last selected options. Thanks @JohnnyWong.
|
||||
* New Popular movie homepage stats. Thanks @JohnnyWong.
|
||||
* Add option for duration vs play count on home stats. (Settings -> Extra Settings). Thanks @JohnnyWong.
|
||||
* Clean up media info pages. Don't show metadata that is missing. Thanks @JohnnyWong.
|
||||
* Add clear button to search inputs. Thanks @JohnnyWong.
|
||||
* New columns on Users list. Thanks @JohnnyWong.
|
||||
* New stream duration option for custom notification messages. Thanks @JohnnyWong.
|
||||
* Rad new tooltips on the history pages. Thanks @JohnnyWong.
|
||||
* And a lot of small visual changes and fixes. Thanks @JohnnyWong.
|
||||
* Fixed IP address modal on user history page.
|
||||
* Fixed "invalid date" showing on monthly plays graph.
|
||||
|
||||
## v1.0.1 (2015-08-13)
|
||||
## v1.1.2 (2015-08-16)
|
||||
|
||||
* Allow SSL certificate check override for certain systems with bad CA stores.
|
||||
* Fix typo on graphs page causing date selection to break on Safari.
|
||||
* Fix bug where user refresh would fail under certain circumstances.
|
||||
|
||||
## v1.1.1 (2015-08-15)
|
||||
|
||||
* Added Most watched movie for home stats. Thanks @jroyal.
|
||||
* Added TV show title to recently added text. Thanks @jroyal.
|
||||
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
|
||||
* Fix bug where custom avatar URL would get reset on every user refresh.
|
||||
|
||||
## v1.1.0 (2015-08-15)
|
||||
|
||||
@@ -23,3 +44,12 @@
|
||||
* Fix alignment of bands on daily graphs which highlight weekends.
|
||||
* Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close.
|
||||
* Fix some styling niggles.
|
||||
|
||||
## v1.0.1 (2015-08-13)
|
||||
|
||||
* Allow SSL certificate check override for certain systems with bad CA stores.
|
||||
* Fix typo on graphs page causing date selection to break on Safari.
|
||||
|
||||
## v1.0 (2015-08-11)
|
||||
|
||||
* First release
|
61
README.md
61
README.md
@@ -6,7 +6,7 @@ This project is based on code from Headphones (https://github.com/rembo10/headph
|
||||
|
||||
* plexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
|
||||
|
||||
If you'd like to buy me a beer, hit the donate button below.
|
||||
If you'd like to buy me a beer, hit the donate button below. All donations go to the project maintainer (primarily for the procurement of liquid refreshment).
|
||||
|
||||
[](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
|
||||
* 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
|
||||
|
||||
* Global watching history with search/filtering & dynamic column sorting
|
||||
* date
|
||||
* user
|
||||
* platform
|
||||
* ip address (if enabled in plexWatch)
|
||||
* ip address
|
||||
* title
|
||||
* stream information details
|
||||
* start time
|
||||
* paused duration length
|
||||
* stop time
|
||||
* duration length
|
||||
* percentage completed
|
||||
* watched progress
|
||||
* show/hide columns
|
||||
* delete mode - allows deletion of specific history items
|
||||
|
||||
* Full user list with general information and comparison stats
|
||||
|
||||
* Individual user information
|
||||
- username and gravatar (if available)
|
||||
- daily, weekly, monthly, all time stats for play count and duration length
|
||||
- individual platform stats for each user
|
||||
- public ip address history with last seen date and geo tag location
|
||||
- recently watched content
|
||||
- watching history
|
||||
- synced items
|
||||
* username and gravatar (if available)
|
||||
* daily, weekly, monthly, all time stats for play count and duration length
|
||||
* individual platform stats for each user
|
||||
* public ip address history with last seen date and geo tag location
|
||||
* recently watched content
|
||||
* watching history
|
||||
* synced items
|
||||
* assign users custom friendly names within PlexPy
|
||||
* assign users custom avatar URL within PlexPy
|
||||
* disable history logging per user
|
||||
* disable notifications per user
|
||||
* option to purge all history per user.
|
||||
|
||||
* Rich analytics presented using Highcharts graphing
|
||||
- user-selectable time periods of 30, 90 or 365 days
|
||||
- daily watch count and duration
|
||||
- totals by day of week and hours of the day
|
||||
- totals by top 10 platform
|
||||
- totals by top 10 users
|
||||
- detailed breakdown by transcode decision
|
||||
- source and stream resolutions
|
||||
- transcode decision counts by user and platform
|
||||
- total monthly counts
|
||||
* user-selectable time periods of 30, 90 or 365 days
|
||||
* daily watch count and duration
|
||||
* totals by day of week and hours of the day
|
||||
* totals by top 10 platform
|
||||
* totals by top 10 users
|
||||
* detailed breakdown by transcode decision
|
||||
* source and stream resolutions
|
||||
* transcode decision counts by user and platform
|
||||
* total monthly counts
|
||||
|
||||
* Content information pages
|
||||
- movies (includes watching history)
|
||||
- tv shows (includes watching history)
|
||||
- tv seasons
|
||||
- tv episodes (includes watching history)
|
||||
* movies (includes watching history)
|
||||
* tv shows (includes watching history)
|
||||
* tv seasons
|
||||
* tv episodes (includes watching history)
|
||||
|
||||
* Full sync list data on all users syncing items from your library
|
||||
|
||||
|
@@ -75,6 +75,7 @@ ul.ColVis_collection {
|
||||
background-color: #444;
|
||||
overflow: hidden;
|
||||
z-index: 2002;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
ul.ColVis_collection li {
|
||||
|
@@ -251,15 +251,9 @@ fieldset[disabled] .btn-bright.active {
|
||||
background-color: #222222;
|
||||
}
|
||||
.modal-body table {
|
||||
color: #999;
|
||||
}
|
||||
.modal-body ul {
|
||||
list-style: none;
|
||||
-webkit-padding-start: 0px;
|
||||
margin: 0;
|
||||
color: #fff;
|
||||
}
|
||||
.modal-body li {
|
||||
list-style: none;
|
||||
margin-top: 7px;
|
||||
margin-left: 4px;
|
||||
color: #aaa;
|
||||
@@ -270,6 +264,9 @@ fieldset[disabled] .btn-bright.active {
|
||||
.modal-body i {
|
||||
color: #F9AA03;
|
||||
}
|
||||
.modal-body i.fa {
|
||||
color: #fff;
|
||||
}
|
||||
.modal-body strong {
|
||||
color: #F9AA03;
|
||||
}
|
||||
@@ -407,39 +404,31 @@ input[type="color"],
|
||||
}
|
||||
.poster {
|
||||
float: left;
|
||||
min-height: 232px;
|
||||
min-width: 155px;
|
||||
min-height: 225px;
|
||||
min-width: 150px;
|
||||
margin-bottom: 8px;
|
||||
position: relative;
|
||||
}
|
||||
.poster-face img {
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
.poster-face {
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
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;
|
||||
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;
|
||||
overflow: hidden;
|
||||
height: 153px;
|
||||
width: 153px;
|
||||
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;
|
||||
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);
|
||||
}
|
||||
.users-poster-face img {
|
||||
bottom: 0;
|
||||
@@ -524,7 +513,7 @@ input[type="color"],
|
||||
left: 0;
|
||||
float: left;
|
||||
text-align: left;
|
||||
padding: 2px 2px 6px 2px;
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
text-overflow: ellipsis;
|
||||
@@ -538,7 +527,7 @@ input[type="color"],
|
||||
right: 0;
|
||||
float: right;
|
||||
text-align: right;
|
||||
padding: 2px 2px 6px 2px;
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
color: #eee;
|
||||
}
|
||||
@@ -630,10 +619,16 @@ input[type="color"],
|
||||
}
|
||||
.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 {
|
||||
margin-right: 27px;
|
||||
position: relative;
|
||||
float: left;
|
||||
min-height: 340px;
|
||||
}
|
||||
.dashboard-recent-media-metacontainer{
|
||||
width: 153px;
|
||||
@@ -642,7 +637,7 @@ input[type="color"],
|
||||
clear: both;
|
||||
}
|
||||
.dashboard-recent-media-metacontainer h3 {
|
||||
padding-top: 5px;
|
||||
padding: 5px 3px 0 3px;
|
||||
color: #fff;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -650,18 +645,19 @@ input[type="color"],
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
line-height: 15px;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
width: 153px;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
clear: both;
|
||||
}
|
||||
.dashboard-recent-media-metacontainer text-muted {
|
||||
.dashboard-recent-media-metacontainer .text-muted {
|
||||
padding: 5px 3px 0 3px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
text-align: left;
|
||||
}
|
||||
.art-face {
|
||||
background-repeat: no-repeat;
|
||||
@@ -862,6 +858,25 @@ input[type="color"],
|
||||
}
|
||||
.summary-content-director strong {
|
||||
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 {
|
||||
float: left;
|
||||
@@ -870,6 +885,7 @@ input[type="color"],
|
||||
}
|
||||
.summary-content-duration strong {
|
||||
color: #fff;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.summary-content-content-rating {
|
||||
float: left;
|
||||
@@ -878,6 +894,7 @@ input[type="color"],
|
||||
}
|
||||
.summary-content-content-rating strong {
|
||||
color: #fff;
|
||||
margin-left: 2px;
|
||||
}
|
||||
.summary-content-summary {
|
||||
overflow: hidden;
|
||||
@@ -890,6 +907,9 @@ input[type="color"],
|
||||
max-height: 160px;
|
||||
padding-bottom: 0px;
|
||||
}
|
||||
.summary-content-people-wrapper {
|
||||
margin-top: 25px;
|
||||
}
|
||||
.summary-content-people-wrapper hidden-phone hidden-tablet {
|
||||
overflow: hidden;
|
||||
height: auto;
|
||||
@@ -897,13 +917,16 @@ input[type="color"],
|
||||
}
|
||||
.summary-content-actors {
|
||||
margin-top: 0px;
|
||||
margin-left: 35px;
|
||||
margin-right: 35px;
|
||||
margin-left: 0px;
|
||||
margin-right: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #999;
|
||||
overflow: hidden;
|
||||
}
|
||||
.summary-content-actors ul {
|
||||
padding-left:20px;
|
||||
}
|
||||
.summary-content-actors li {
|
||||
list-style: none;
|
||||
list-style-image: none;
|
||||
@@ -912,15 +935,38 @@ input[type="color"],
|
||||
line-height: 18px;
|
||||
color: #fff;
|
||||
}
|
||||
.summary-content-writers {
|
||||
margin-top: 10px;
|
||||
margin-left: 35px;
|
||||
margin-right: 35px;
|
||||
.summary-content-genres {
|
||||
margin-top: 0px;
|
||||
margin-left: 0px;
|
||||
margin-right: 15px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: #999;
|
||||
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 {
|
||||
list-style: none;
|
||||
list-style-image: none;
|
||||
@@ -959,18 +1005,32 @@ input[type="color"],
|
||||
list-style: none;
|
||||
margin: 0 0 0px 0px;
|
||||
}
|
||||
.season-episodes-instance>li {
|
||||
.season-episodes-instance li {
|
||||
float: left;
|
||||
height: 200px;
|
||||
width: 250px;
|
||||
position: relative;
|
||||
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 {
|
||||
float: left;
|
||||
position: relative;
|
||||
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 {
|
||||
bottom: 0;
|
||||
overflow: hidden;
|
||||
@@ -987,11 +1047,14 @@ input[type="color"],
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
padding: 4px 8px;
|
||||
text-align: left;
|
||||
background-color: #000;
|
||||
background-color: rgba(0,0,0,0.7);
|
||||
border-top: 1px solid #000;
|
||||
background: -moz-linear-gradient(top, rgba(0,0,0,0) 30%, rgba(0,0,0,1) 100%);
|
||||
background: -webkit-gradient(linear, left top, left bottom, color-stop(30%,rgba(0,0,0,0)), color-stop(100%,rgba(0,0,0,1)));
|
||||
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 {
|
||||
color: #fff;
|
||||
@@ -999,10 +1062,27 @@ input[type="color"],
|
||||
text-shadow: 0 1px 5px rgba(0,0,0,0.2);
|
||||
}
|
||||
.season-episodes-instance-text-wrapper {
|
||||
width: 250px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 20px;
|
||||
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;
|
||||
font-size: 14px;
|
||||
font-weight: normal;
|
||||
@@ -1015,6 +1095,7 @@ input[type="color"],
|
||||
width: 205px;
|
||||
margin-top: 2px;
|
||||
margin-left: 0px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.season-episodes a:hover {
|
||||
color: #F9AA03;
|
||||
@@ -1023,6 +1104,9 @@ input[type="color"],
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
float: left;
|
||||
position: absolute;
|
||||
left: 8px;
|
||||
bottom: 5px;
|
||||
}
|
||||
.user-info-wrapper {
|
||||
height: 113px;
|
||||
@@ -1078,7 +1162,10 @@ input[type="color"],
|
||||
margin: 0px 0px 0px 0px;
|
||||
}
|
||||
.user-overview-stats-instance {
|
||||
line-height: 11px;
|
||||
float: left;
|
||||
width: 350px;
|
||||
height: 80px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.user-overview-stats-instance-device_icon {
|
||||
float: left;
|
||||
@@ -1100,25 +1187,27 @@ input[type="color"],
|
||||
.user-overview-stats-instance p {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
float:left;
|
||||
float: left;
|
||||
position: relative;
|
||||
top: 30px;
|
||||
top: 14px;
|
||||
left: 0px;
|
||||
}
|
||||
.user-overview-stats-instance h3 strong{
|
||||
color: #fff;
|
||||
}
|
||||
.user-overview-stats-instance h3 {
|
||||
font-size: 30px;
|
||||
font-weight: bold;
|
||||
color: #F9AA03;
|
||||
line-height: 30px;
|
||||
line-height: 22px;
|
||||
position: relative;
|
||||
top: 0px;
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
top: 5px;
|
||||
margin: 0 5px 0 10px;
|
||||
float: left;
|
||||
}
|
||||
.user-overview-stats-instance h4 {
|
||||
color: #fff;
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.user-overview-stats-instance h1 {
|
||||
font-size: 54px;
|
||||
@@ -1138,7 +1227,7 @@ input[type="color"],
|
||||
.user-platforms-instance {
|
||||
float: left;
|
||||
width: 240px;
|
||||
height: 90px;
|
||||
height: 80px;
|
||||
margin-bottom: 25px;
|
||||
}
|
||||
.user-platforms-instance li {
|
||||
@@ -1150,6 +1239,10 @@ input[type="color"],
|
||||
-webkit-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);
|
||||
background-size: contain;
|
||||
position: relative;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
}
|
||||
.user-platforms-instance-name {
|
||||
float: left;
|
||||
@@ -1160,7 +1253,7 @@ input[type="color"],
|
||||
position: relative;
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
font-weight: bold;
|
||||
font-weight: normal;
|
||||
width: 140px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
@@ -1179,26 +1272,39 @@ input[type="color"],
|
||||
font-size: 12px;
|
||||
float: left;
|
||||
position: relative;
|
||||
top: 15px;
|
||||
top: 14px;
|
||||
left: 0px;
|
||||
}
|
||||
.home-platforms {
|
||||
max-width: 1600px;
|
||||
}
|
||||
.home-platforms ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
}
|
||||
.home-platforms-instance-poster {
|
||||
float: left;
|
||||
margin-left: 0px;
|
||||
}
|
||||
.home-platforms-instance-poster .poster-face {
|
||||
height: 120px;
|
||||
max-width: 81px;
|
||||
padding: 5px 10px 5px 10px;
|
||||
width: 80px;
|
||||
}
|
||||
.home-platforms-instance-box {
|
||||
float: left;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 35px;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
padding: 25px 10px 25px 10px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.home-platforms-instance-oval {
|
||||
float: left;
|
||||
background-size: contain;
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
bottom: 35px;
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
padding: 25px 10px 25px 10px;
|
||||
-webkit-border-radius: 50%;
|
||||
-moz-border-radius: 50%;
|
||||
border-radius: 50%;
|
||||
@@ -1212,21 +1318,61 @@ input[type="color"],
|
||||
font-size: 13px;
|
||||
line-height: 15px;
|
||||
font-weight: bold;
|
||||
width: 100%;
|
||||
padding: 10px 0 0 10px;
|
||||
}
|
||||
.home-platforms-instance-playcount {
|
||||
float: left;
|
||||
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 {
|
||||
background-color: #282828;
|
||||
position: relative;
|
||||
float: left;
|
||||
width: 300px;
|
||||
height: 130px;
|
||||
padding: 5px;
|
||||
background-color: #282828;
|
||||
height: 120px;
|
||||
padding: 10px;
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
webkit-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 {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
@@ -1500,6 +1646,30 @@ input[type="color"],
|
||||
width: 250px;
|
||||
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 {
|
||||
background-color: #444;
|
||||
color: #999999;
|
||||
|
@@ -31,7 +31,7 @@ DOCUMENTATION :: END
|
||||
<div class="form-group">
|
||||
<label for="friendly_name">Friendly Name</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="col-md-6">
|
||||
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${data['friendly_name']}" size="30">
|
||||
</div>
|
||||
</div>
|
||||
@@ -40,11 +40,11 @@ DOCUMENTATION :: END
|
||||
<div class="form-group">
|
||||
<label for="profile_url">Profile Picture URL</label>
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<div class="col-md-8">
|
||||
<input type="text" class="form-control" id="profile_url" name="profile_url" value="${data['thumb']}">
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Change the users profile picture in plexpy. You should save the URL if you would like to go back as this replaces the existing one.</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 class="checkbox">
|
||||
<label>
|
||||
@@ -58,6 +58,12 @@ DOCUMENTATION :: END
|
||||
</label>
|
||||
<p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p>
|
||||
</div>
|
||||
% if data['user_id']:
|
||||
<div class="form-group">
|
||||
<button class="btn btn-danger" id="delete-all-history">Purge</button>
|
||||
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
|
||||
</div>
|
||||
% endif
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -112,6 +118,21 @@ DOCUMENTATION :: END
|
||||
});
|
||||
% endif
|
||||
});
|
||||
|
||||
$("#delete-all-history").click(function() {
|
||||
var r = confirm("Are you REALLY REALLY REALLY sure you want to delete all history for this user?");
|
||||
if (r == true) {
|
||||
$.ajax({
|
||||
url: 'delete_all_user_history',
|
||||
data: {user_id: '${data['user_id']}'},
|
||||
cache: false,
|
||||
async: true,
|
||||
success: function(data) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
% endif
|
@@ -263,10 +263,37 @@
|
||||
<script>
|
||||
$(document).ready(function () {
|
||||
|
||||
var current_range = 30;
|
||||
// Save graph state to cookies
|
||||
$('input[name=yaxis-options]').change(function() {
|
||||
setCookie('graphType', $(this).val(), 365, '/');
|
||||
});
|
||||
$('input[name=date-options]').change(function() {
|
||||
setCookie('graphDate', $(this).val(), 365, '/');
|
||||
});
|
||||
$('a[data-toggle=tab]').click(function() {
|
||||
setCookie('graphTab', $(this).attr('href'), 365, '/');
|
||||
});
|
||||
|
||||
// Initial values for graph if no saved state
|
||||
var yaxis = 'plays';
|
||||
var current_range = 30;
|
||||
var current_tab = '#tabs-1';
|
||||
|
||||
// Read saved graph state from cookies and set initial values
|
||||
if(getCookie('graphType')) {
|
||||
var yaxis = getCookie('graphType');
|
||||
$('input[name=yaxis-options][value=' + yaxis + ']').prop('checked', true).trigger('click');
|
||||
}
|
||||
if(getCookie('graphDate')) {
|
||||
var current_range = getCookie('graphDate');
|
||||
$('input[name=date-options][value=' + current_range + ']').prop('checked', true).trigger('click');
|
||||
$('.days').html(current_range);
|
||||
}
|
||||
if(getCookie('graphTab')) {
|
||||
var current_tab = getCookie('graphTab');
|
||||
$('a[data-toggle=tab][href=' + current_tab + ']').trigger('click');
|
||||
}
|
||||
|
||||
function loadGraphsTab1(time_range, yaxis) {
|
||||
setGraphFormat(yaxis);
|
||||
|
||||
@@ -432,12 +459,8 @@
|
||||
data: { y_axis: yaxis },
|
||||
dataType: "json",
|
||||
success: function(data) {
|
||||
var dateArray = [];
|
||||
for (var i = 0; i < data.categories.length; i++) {
|
||||
dateArray.push(moment(data.categories[i], 'YYYY-MM').format('MMM YYYY'));
|
||||
}
|
||||
hc_plays_by_month_options.yAxis.min = 0;
|
||||
hc_plays_by_month_options.xAxis.categories = dateArray;
|
||||
hc_plays_by_month_options.xAxis.categories = data.categories;
|
||||
hc_plays_by_month_options.series = data.series;
|
||||
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
|
||||
}
|
||||
|
@@ -13,23 +13,26 @@
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-history"></i> History</span>
|
||||
</div>
|
||||
<div class="colvis-button-bar hidden-xs">
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete mode</button> 
|
||||
<div class="colvis-button-bar hidden-xs"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align='left' id="delete_row">Delete</th>
|
||||
<th align='left' id="time">Time</th>
|
||||
<th align='left' id="friendly_name">User</th>
|
||||
<th align='left' id="platform">Platform</th>
|
||||
<th align='left' id="ip_address">IP Address</th>
|
||||
<th align='left' id="platform">Platform</th>
|
||||
<th align='left' id="title">Title</th>
|
||||
<th align='left' id="started">Started</th>
|
||||
<th align='left' id="paused_counter">Paused</th>
|
||||
<th align='left' id="stopped">Stopped</th>
|
||||
<th align='left' id="duration">Duration</th>
|
||||
<th align='left' id="percent_complete">Watched</th>
|
||||
<th align='left' id="percent_complete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -60,8 +63,22 @@
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
|
||||
$('#row-edit-mode').click(function() {
|
||||
if ($(this).hasClass('active')) {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).addClass('hidden');
|
||||
});
|
||||
} else {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
</script>
|
||||
|
@@ -43,6 +43,8 @@
|
||||
}
|
||||
|
||||
history_table = $('#history_table').DataTable(history_table_modal_options);
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
});
|
||||
</script>
|
||||
% else:
|
||||
|
@@ -9,7 +9,8 @@ Variable names: data [array]
|
||||
|
||||
data[array_index] :: Usable parameters
|
||||
|
||||
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'popular_tv', 'top_user' or 'top_platform'
|
||||
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'top_movies', 'popular_tv', 'popular_movies', 'top_user' or 'top_platform'
|
||||
data['stat_type'] Returns the type of the stat. Either 'total_plays' or 'total_duration'
|
||||
data['rows'] Returns an array containing stat data
|
||||
|
||||
data[array_index]['rows'] :: Usable parameters
|
||||
@@ -21,10 +22,11 @@ grandparent_thumb Returns location of the item's thumbnail. Use with pms_i
|
||||
rating_key Returns the unique identifier for the media item.
|
||||
title Returns the title for the associated stat.
|
||||
|
||||
== Only if 'stat_id' is 'top_tv' or 'top_user' or 'top_platform' ==
|
||||
== Only if 'stat_id' is 'top_tv' or 'top_movies' or 'top_user' or 'top_platform' ==
|
||||
total_plays Returns the count for the associated stat.
|
||||
total_duration Returns the total duration for the associated stat.
|
||||
|
||||
== Only of 'stat_id' is 'popular_tv' ==
|
||||
== Only of 'stat_id' is 'popular_tv' or 'popular_movies' ==
|
||||
users_watched Returns the count for the associated stat.
|
||||
|
||||
== Only if 'stat_id' is 'top_user' ==
|
||||
@@ -39,6 +41,22 @@ platform_type Returns the platform name for the associated stat.
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
|
||||
<%!
|
||||
from plexpy import helpers
|
||||
|
||||
# Human readable duration
|
||||
def hd(minutes):
|
||||
if int(minutes) > 60:
|
||||
hours = int(helpers.cast_to_float(minutes) / 60)
|
||||
minutes = int(helpers.cast_to_float(minutes) % hours)
|
||||
if minutes > 0:
|
||||
return "<h3>" + str(hours) + "</h3><p>hrs</p><h3>" + str(minutes) + "</h3><p>mins</p>"
|
||||
else:
|
||||
return "<h3>" + str(hours) + "</h3><p>hrs</p>"
|
||||
else:
|
||||
return "<h3>" + minutes + "</h3><p>mins</p>"
|
||||
%>
|
||||
|
||||
% if data:
|
||||
% if data[0]['rows'] or data[2]['rows']:
|
||||
<ul class="list-unstyled">
|
||||
@@ -46,16 +64,7 @@ DOCUMENTATION :: END
|
||||
% if a['stat_id'] == 'top_tv' and a['rows']:
|
||||
<div class="home-platforms-instance">
|
||||
<li>
|
||||
<span>
|
||||
<a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
% if a['rows'][0]['grandparent_thumb']:
|
||||
<img class="home-platforms-instance-poster"
|
||||
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
|
||||
% else:
|
||||
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
|
||||
% endif
|
||||
</a>
|
||||
</span>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Most Watched TV</h4>
|
||||
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
@@ -63,24 +72,31 @@ DOCUMENTATION :: END
|
||||
</a></h5>
|
||||
</div>
|
||||
<div class="user-platforms-instance-playcount">
|
||||
% if a['stat_type'] == 'total_plays':
|
||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
||||
<p> plays</p>
|
||||
% else:
|
||||
${a['rows'][0]['total_duration'] | hd}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
% if a['rows'][0]['grandparent_thumb']:
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||
</div>
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
% elif a['stat_id'] == 'popular_tv' and a['rows']:
|
||||
<div class="home-platforms-instance">
|
||||
<li>
|
||||
<span>
|
||||
<a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
% if a['rows'][0]['grandparent_thumb'] != '':
|
||||
<img class="home-platforms-instance-poster"
|
||||
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
|
||||
% else:
|
||||
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
|
||||
% endif
|
||||
</a>
|
||||
</span>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Most Popular TV</h4>
|
||||
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
@@ -91,26 +107,84 @@ DOCUMENTATION :: END
|
||||
<h3>${a['rows'][0]['users_watched']}</h3>
|
||||
<p> users</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
% if a['rows'][0]['grandparent_thumb'] != '':
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
</div>
|
||||
% else:
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
|
||||
</div>
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
% elif a['stat_id'] == 'top_movies' and a['rows']:
|
||||
<div class="home-platforms-instance">
|
||||
<li>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Most Watched Movie</h4>
|
||||
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
|
||||
${a['rows'][0]['title']}
|
||||
</a></h5>
|
||||
</div>
|
||||
<div class="user-platforms-instance-playcount">
|
||||
% if a['stat_type'] == 'total_plays':
|
||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
||||
<p> plays</p>
|
||||
% else:
|
||||
${a['rows'][0]['total_duration'] | hd}
|
||||
% endif
|
||||
</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>
|
||||
</div>
|
||||
% elif a['stat_id'] == 'top_users' and a['rows']:
|
||||
<div class="home-platforms-instance">
|
||||
<li>
|
||||
<span>
|
||||
% if a['rows'][0]['user_id']:
|
||||
<a href="user?user_id=${a['rows'][0]['user_id']}">
|
||||
% else:
|
||||
<a href="user?user=${a['rows'][0]['user']}">
|
||||
% endif
|
||||
% if a['rows'][0]['thumb'] != '':
|
||||
<img class="home-platforms-instance-oval" src="${a['rows'][0]['thumb']}"
|
||||
class="poster-face">
|
||||
% else:
|
||||
<img class="home-platforms-instance-oval"
|
||||
src="interfaces/default/images/gravatar-default.png">
|
||||
% endif
|
||||
</a>
|
||||
</span>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Most Active User</h4>
|
||||
<h5>
|
||||
@@ -124,29 +198,55 @@ DOCUMENTATION :: END
|
||||
</h5>
|
||||
</div>
|
||||
<div class="user-platforms-instance-playcount">
|
||||
% if a['stat_type'] == 'total_plays':
|
||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
||||
<p> plays</p>
|
||||
% else:
|
||||
${a['rows'][0]['total_duration'] | hd}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
% if a['rows'][0]['user_id']:
|
||||
<a href="user?user_id=${a['rows'][0]['user_id']}">
|
||||
% else:
|
||||
<a href="user?user=${a['rows'][0]['user']}">
|
||||
% endif
|
||||
% if a['rows'][0]['thumb'] != '':
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="home-platforms-instance-oval" style="background-image: url(${a['rows'][0]['thumb']});">
|
||||
</div>
|
||||
% else:
|
||||
<div class="home-platforms-instance-poster">
|
||||
<div class="home-platforms-instance-oval" style="background-image: url(interfaces/default/images/gravatar-default.png);">
|
||||
</div>
|
||||
% endif
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
% elif a['stat_id'] == 'top_platforms' and a['rows']:
|
||||
<div class="home-platforms-instance">
|
||||
<li>
|
||||
<div id="platform-stat">
|
||||
<img class="home-platforms-instance-box" src="interfaces/default/images/platforms/default.png">
|
||||
</div>
|
||||
<div class="home-platforms-instance-info">
|
||||
<div class="home-platforms-instance-name">
|
||||
<h4>Most Active Platform</h4>
|
||||
<h5>${a['rows'][0]['platform_type']}</h5>
|
||||
</div>
|
||||
<div class="user-platforms-instance-playcount">
|
||||
% if a['stat_type'] == 'total_plays':
|
||||
<h3>${a['rows'][0]['total_plays']}</h3>
|
||||
<p> plays</p>
|
||||
% else:
|
||||
${a['rows'][0]['total_duration'] | hd}
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<div id="platform-stat" class="home-platforms-instance-poster">
|
||||
<div class="home-platforms-instance-box" style="background-image: url(interfaces/default/images/platforms/default.png);">
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<script>
|
||||
$("#platform-stat").html("<img class='home-platforms-instance-box' src='" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + "'>");
|
||||
$("#platform-stat").html("<div class='home-platforms-instance-box' style='background-image: url(" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + ");'>");
|
||||
</script>
|
||||
% endif
|
||||
% endfor
|
||||
|
@@ -21,7 +21,7 @@
|
||||
<div class="padded-header">
|
||||
<h3>Statistics <small>Last ${config['home_stats_length']} days</small></h3>
|
||||
</div>
|
||||
<div id="home-stats" class="user-platforms">
|
||||
<div id="home-stats" class="home-platforms">
|
||||
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
|
||||
<br>
|
||||
</div>
|
||||
@@ -45,12 +45,12 @@
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
<script>
|
||||
|
||||
function getHomeStats(days) {
|
||||
function getHomeStats(days, plays) {
|
||||
$.ajax({
|
||||
url: 'home_stats',
|
||||
cache: false,
|
||||
async: true,
|
||||
data: {time_range: days},
|
||||
data: {time_range: days, stat_type: plays},
|
||||
complete: function(xhr, status) {
|
||||
$("#home-stats").html(xhr.responseText);
|
||||
}
|
||||
@@ -110,7 +110,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
getHomeStats(${config['home_stats_length']});
|
||||
getHomeStats(${config['home_stats_length']}, ${config['home_stats_type']});
|
||||
|
||||
|
||||
</script>
|
||||
|
@@ -30,6 +30,7 @@ genres Returns an array of genres.
|
||||
actors Returns an array of actors.
|
||||
directors Returns an array of directors.
|
||||
studio Returns the name of the studio.
|
||||
originally_available_at Returns the air date of the item.
|
||||
|
||||
DOCUMENTATION :: END
|
||||
</%doc>
|
||||
@@ -53,46 +54,58 @@ DOCUMENTATION :: END
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<div class="summary-content-poster hidden-xs hidden-sm">
|
||||
% if data['type'] == 'episode':
|
||||
<img src="pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster">
|
||||
% if data['type'] == 'episode' and data['parent_thumb']:
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% elif data['type'] == 'episode':
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% else:
|
||||
<img src="pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster">
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content">
|
||||
<div class="summary-content-title">
|
||||
% if data['type'] == 'movie':
|
||||
<h1>${data['title']} (${data['year']})</h1>
|
||||
<h1>${data['title']}</h1>
|
||||
% elif data['type'] == 'season':
|
||||
<h1>${data['parent_title']} (${data['title']})</h1>
|
||||
% elif data['type'] == 'episode':
|
||||
<h1>${data['grandparent_title']} (Season ${data['parent_index']}, Episode
|
||||
${data['index']}) "${data['title']}"</h1>
|
||||
<h1>${data['grandparent_title']} - ${data['title']}
|
||||
(Season ${data['parent_index']}, Episode ${data['index']})</h1>
|
||||
% else:
|
||||
<h1>${data['title']}</h1>
|
||||
% endif
|
||||
</div>
|
||||
% if data['type'] == 'movie':
|
||||
% if (data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode') and data['rating']:
|
||||
<div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
|
||||
data-rateit-ispreset="true" data-rateit-readonly="true"></div>
|
||||
% endif
|
||||
<div class="summary-content-details-wrapper">
|
||||
<div class="summary-content-director">
|
||||
% if data['type'] == 'episode' or data['type'] == 'movie':
|
||||
% if data['directors']:
|
||||
% if (data['type'] == 'episode' or data['type'] == 'movie') and data['directors']:
|
||||
Directed by <strong> ${data['directors'][0]}</strong>
|
||||
% else:
|
||||
Directed by <strong> unknown</strong>
|
||||
% endif
|
||||
% elif data['type'] == 'show' or data['type'] == 'season':
|
||||
</div>
|
||||
<div class="summary-content-studio">
|
||||
% if (data['type'] == 'show' or data['type'] == 'movie') and data['studio']:
|
||||
Studio <strong> ${data['studio']}</strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-airdate">
|
||||
% if data['type'] == 'movie':
|
||||
Year <strong> ${data['year']}</strong>
|
||||
% elif data['type'] == 'show':
|
||||
Aired <strong> ${data['year']}</strong>
|
||||
% elif data['type'] == 'episode':
|
||||
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
|
||||
% endif
|
||||
</div>
|
||||
<div class="summary-content-duration">
|
||||
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
|
||||
</div>
|
||||
<div class="summary-content-content-rating">
|
||||
% if (data['type'] == 'episode' or data['type'] == 'movie' or data['type'] == 'show') and data['content_rating']:
|
||||
Rated <strong> ${data['content_rating']} </strong>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="summary-content-summary">
|
||||
@@ -100,9 +113,23 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if data['type'] == 'episode':
|
||||
<div class="col-md-3">
|
||||
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
||||
% if (data['type'] == 'movie' or data['type'] == 'show') and data['genres']:
|
||||
<div class="summary-content-genres">
|
||||
<strong>Genres</strong>
|
||||
<ul>
|
||||
% for genre in data['genres']:
|
||||
% if loop.index < 5:
|
||||
<li>
|
||||
${genre}
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
% if (data['type'] == 'episode' or data['type'] == 'movie') and data['writers']:
|
||||
<div class="summary-content-writers">
|
||||
<strong>Written by</strong>
|
||||
<ul>
|
||||
@@ -115,24 +142,8 @@ DOCUMENTATION :: END
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['type'] == 'movie' or data['type'] == 'show':
|
||||
<div class="col-md-3">
|
||||
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
||||
<div class="summary-content-actors">
|
||||
<strong>Genres</strong>
|
||||
<ul>
|
||||
% for genre in data['genres']:
|
||||
% if loop.index < 5:
|
||||
<li>
|
||||
${genre}
|
||||
</li>
|
||||
% endif
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
|
||||
% if (data['type'] == 'movie' or data['type'] == 'show') and data['actors']:
|
||||
<div class="summary-content-actors">
|
||||
<strong>Starring</strong>
|
||||
<ul>
|
||||
@@ -145,11 +156,6 @@ DOCUMENTATION :: END
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% elif data['type'] == 'season':
|
||||
<div class="col-md-3"></div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
@@ -157,6 +163,8 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show':
|
||||
<div class='container-fluid'>
|
||||
@@ -164,25 +172,28 @@ DOCUMENTATION :: END
|
||||
<div class='col-md-12'>
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>Watch history for <strong>${data['title']}</strong></span>
|
||||
<span>Watch History for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
<div class="colvis-button-bar hidden-xs">
|
||||
<div class="button-bar">
|
||||
<button class="btn btn-danger" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode"><i class="fa fa-trash-o"></i> Delete mode</button> 
|
||||
<div class="colvis-button-bar hidden-xs"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align='left' id="delete">Delete</th>
|
||||
<th align='left' id="time">Time</th>
|
||||
<th align='left' id="friendly_name">User</th>
|
||||
<th align='left' id="platform">Platform</th>
|
||||
<th align='left' id="ip_address">IP Address</th>
|
||||
<th align='left' id="platform">Platform</th>
|
||||
<th align='left' id="title">Title</th>
|
||||
<th align='left' id="started">Started</th>
|
||||
<th align='left' id="paused_counter">Paused</th>
|
||||
<th align='left' id="stopped">Stopped</th>
|
||||
<th align='left' id="duration">Duration</th>
|
||||
<th align='left' id="percent_complete">Watched</th>
|
||||
<th align='left' id="percent_complete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -202,7 +213,7 @@ DOCUMENTATION :: END
|
||||
<div class='col-md-12'>
|
||||
<div class='table-card-header'>
|
||||
<div class="header-bar">
|
||||
<span>Episode list for <strong>${data['title']}</strong></span>
|
||||
<span>Episode List for <strong>${data['title']}</strong></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class='table-card-back'>
|
||||
@@ -233,7 +244,7 @@ DOCUMENTATION :: END
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
|
||||
% if data:
|
||||
% if data['type'] == 'movie':
|
||||
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode':
|
||||
<script>
|
||||
// Convert rating to 5 star rating type
|
||||
var starRating = Math.round(${data['rating']} / 2)
|
||||
@@ -254,10 +265,23 @@ DOCUMENTATION :: END
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
history_table.column(4).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
|
||||
$('#row-edit-mode').click(function() {
|
||||
if ($(this).hasClass('active')) {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).addClass('hidden');
|
||||
});
|
||||
} else {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% elif data['type'] == 'show':
|
||||
@@ -274,8 +298,22 @@ DOCUMENTATION :: END
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
|
||||
$(colvis.button()).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
|
||||
$('#row-edit-mode').click(function() {
|
||||
if ($(this).hasClass('active')) {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).addClass('hidden');
|
||||
});
|
||||
} else {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
% endif
|
||||
@@ -294,6 +332,7 @@ DOCUMENTATION :: END
|
||||
</script>
|
||||
% endif
|
||||
<script>
|
||||
$("#airdate").html(moment($("#airdate")).format('MMM DD, YYYY'));
|
||||
$("#runtime").html(millisecondsToMinutes($("#runtime").html(), true));
|
||||
</script>
|
||||
% endif
|
||||
|
@@ -30,25 +30,20 @@ DOCUMENTATION :: END
|
||||
<ul class="season-episodes-instance list-unstyled">
|
||||
% for a in data['episode_list']:
|
||||
<li>
|
||||
<div class="season-episodes-poster">
|
||||
<div class="season-episodes-poster-face">
|
||||
<a href="info?item_id=${a['rating_key']}">
|
||||
<img src="pms_image_proxy?img=${a['thumb']}&width=410&height=230" class="season-episodes-poster-face">
|
||||
</a>
|
||||
</div>
|
||||
<div class="season-episodes-poster">
|
||||
<div class="season-episodes-poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=410&height=230);">
|
||||
<div class="season-episodes-card-overlay">
|
||||
<div class="season-episodes-season">
|
||||
Episode ${a['index']}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="season-episodes-instance-text-wrapper">
|
||||
<div class="season-episodes-title">
|
||||
<a href="info?item_id=${a['rating_key']}">
|
||||
"${a['title']}"
|
||||
<h3>${a['title']}</h3>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
|
@@ -15,7 +15,7 @@
|
||||
<div class="modal-body" id="modal-text">
|
||||
<div class="col-md-6">
|
||||
<h4><strong>Location Details</strong></h4>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li>Country: <strong><span id="country"></span></strong></li>
|
||||
<li>City: <strong><span id="city"></span></strong></li>
|
||||
<li>Region: <strong><span id="region"></span></strong></li>
|
||||
@@ -26,7 +26,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h4><strong>Connection Details</strong></h4>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li>ISP: <strong><span id="isp"></span></strong></li>
|
||||
<li>Organization: <strong><span id="org"></span></strong></li>
|
||||
<li>AS: <strong><span id="as"></span></strong></li>
|
||||
|
@@ -251,6 +251,9 @@ function humanTime(seconds) {
|
||||
} else if (seconds >= 60) {
|
||||
text = '<h3>' + Math.floor(moment.duration(((seconds % 86400) % 3600), 'seconds').asMinutes()) + '</h3><p> mins</p>';
|
||||
return text;
|
||||
} else {
|
||||
text = '<h3>0</h3><p> mins</p>';
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -348,3 +351,12 @@ Accordion.prototype.dropdown = function(e) {
|
||||
$el.find('.submenu').not($next).slideUp().parent().removeClass('open');
|
||||
};
|
||||
}
|
||||
|
||||
function clearSearchButton(tableName, table) {
|
||||
$('#' + tableName + '_filter').find('input[type=search]')
|
||||
.wrap('<div class="input-group" role="group" aria-label="Search"></div>')
|
||||
.after('<span class="input-group-btn"><button class="btn btn-form" data-toggle="button" aria-pressed="false" autocomplete="off" id="clear-search-' + tableName + '"><i class="fa fa-remove"></i></button></span>')
|
||||
$('#clear-search-' + tableName).click(function() {
|
||||
table.search('').draw();
|
||||
});
|
||||
}
|
||||
|
@@ -25,10 +25,22 @@ history_table_options = {
|
||||
"processing": false,
|
||||
"serverSide": true,
|
||||
"pageLength": 25,
|
||||
"order": [ 0, 'desc'],
|
||||
"order": [ 1, 'desc'],
|
||||
"autoWidth": false,
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"data": null,
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
$(td).html('<button class="btn btn-xs btn-danger" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o"></i> Delete</button>');
|
||||
},
|
||||
"width": "5%",
|
||||
"className": "delete-control no-wrap hidden",
|
||||
"searchable": false,
|
||||
"orderable": false
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"data":"date",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (rowData['stopped'] === null) {
|
||||
@@ -38,10 +50,11 @@ history_table_options = {
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "8%",
|
||||
"className": "no-wrap"
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"targets": [2],
|
||||
"data":"friendly_name",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
@@ -54,21 +67,12 @@ history_table_options = {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"width": "8%",
|
||||
"className": "no-wrap hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"data":"player",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
$(td).html('<a href="#" data-target="#info-modal" data-toggle="modal"><i class="fa fa-lg fa-info-circle"></i></a> '+cellData);
|
||||
}
|
||||
},
|
||||
"className": "modal-control no-wrap hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"data":"ip_address",
|
||||
"data": "ip_address",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData) {
|
||||
if (isPrivateIP(cellData)) {
|
||||
@@ -78,35 +82,63 @@ history_table_options = {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
} else {
|
||||
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><i class="fa fa-map-marker"></i> ' + cellData +'</a>');
|
||||
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><i class="fa fa-map-marker"></i> ' + cellData + '</a>');
|
||||
}
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"className": "no-wrap hidden-xs modal-control-ip"
|
||||
"width": "8%",
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"data":"player",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span> ';
|
||||
} 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> ';
|
||||
} 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> ';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||
}
|
||||
},
|
||||
"width": "15%",
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||
},
|
||||
{
|
||||
"targets": [5],
|
||||
"data":"full_title",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode') {
|
||||
transcode_dec = '<i class="fa fa-server"></i> ';
|
||||
}
|
||||
$(td).html('<div><div style="float: left;"><a href="info?source=history&item_id=' + rowData['id'] + '">' + cellData + '</a></div><div style="float: right; text-align: right; padding-right: 5px;">' + transcode_dec + '<i class="fa fa-video-camera"></i></div></div>');
|
||||
var media_type = '';
|
||||
var thumb_popover = '';
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + ' (' + rowData['year'] + ')</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + 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 + ' ' + thumb_popover + '</div></a></div>');
|
||||
} else if (rowData['media_type'] === 'track') {
|
||||
$(td).html('<div><div style="float: left;">' + cellData + '</div><div style="float: right; text-align: right; padding-right: 5px;"><i class="fa fa-music"></i></div></div>');
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
|
||||
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + ' ' + thumb_popover + '</div></div>');
|
||||
} else {
|
||||
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"width": "35%"
|
||||
},
|
||||
{
|
||||
"targets": [5],
|
||||
"targets": [6],
|
||||
"data":"started",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData === null) {
|
||||
@@ -116,10 +148,11 @@ history_table_options = {
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "5%",
|
||||
"className": "no-wrap hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [6],
|
||||
"targets": [7],
|
||||
"data":"paused_counter",
|
||||
"render": function ( data, type, full ) {
|
||||
if (data !== null) {
|
||||
@@ -129,10 +162,11 @@ history_table_options = {
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"className": "no-wrap hidden-xs"
|
||||
"width": "5%",
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [7],
|
||||
"targets": [8],
|
||||
"data":"stopped",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData === null) {
|
||||
@@ -142,10 +176,11 @@ history_table_options = {
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"className": "no-wrap hidden-md hidden-xs"
|
||||
"width": "5%",
|
||||
"className": "no-wrap hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [8],
|
||||
"targets": [9],
|
||||
"data":"duration",
|
||||
"render": function ( data, type, full ) {
|
||||
if (data !== null) {
|
||||
@@ -155,34 +190,50 @@ history_table_options = {
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "5%",
|
||||
"className": "no-wrap hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [9],
|
||||
"targets": [10],
|
||||
"data":"percent_complete",
|
||||
"render": function ( data, type, full ) {
|
||||
if (data > 80) {
|
||||
return '<i class="fa fa-lg fa-circle"></i>'
|
||||
return '<span class="watched-tooltip" data-toggle="tooltip" title="Watched"><i class="fa fa-lg fa-circle"></i></span>'
|
||||
} else if (data > 40) {
|
||||
return '<i class="fa fa-lg fa-adjust fa-rotate-180"></i>'
|
||||
return '<span class="watched-tooltip" data-toggle="tooltip" title="Partial"><i class="fa fa-lg fa-adjust fa-rotate-180"></i></span>'
|
||||
} else {
|
||||
return '<i class="fa fa-lg fa-circle-o"></i>'
|
||||
return '<span class="watched-tooltip" data-toggle="tooltip" title="Unwatched"><i class="fa fa-lg fa-circle-o"></i></span>'
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"orderable": false,
|
||||
"className": "no-wrap hidden-md hidden-xs",
|
||||
"width": "10px"
|
||||
}
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs",
|
||||
"width": "1%"
|
||||
},
|
||||
],
|
||||
"drawCallback": function (settings) {
|
||||
// Jump to top of page
|
||||
// $('html,body').scrollTop(0);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
|
||||
// Create the tooltips.
|
||||
$('.info-modal').each(function() {
|
||||
$(this).tooltip();
|
||||
$('.transcode-tooltip').tooltip();
|
||||
$('.media-type-tooltip').tooltip();
|
||||
$('.watched-tooltip').tooltip();
|
||||
$('.thumb-tooltip').popover({
|
||||
html: true,
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
content: function () {
|
||||
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
|
||||
}
|
||||
});
|
||||
|
||||
if ($('#row-edit-mode').hasClass('active')) {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
},
|
||||
"preDrawCallback": function(settings) {
|
||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
||||
@@ -228,6 +279,24 @@ $('#history_table').on('click', 'td.modal-control-ip', function () {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getUserLocation(rowData['ip_address']);
|
||||
});
|
||||
|
||||
$('#history_table').on('click', 'td.delete-control > button', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = history_table.row( tr );
|
||||
var rowData = row.data();
|
||||
|
||||
$(this).prop('disabled', true);
|
||||
$(this).html('<i class="fa fa-spin fa-refresh"></i> Delete');
|
||||
|
||||
$.ajax({
|
||||
url: 'delete_history_rows',
|
||||
data: {row_id: rowData['id']},
|
||||
async: true,
|
||||
success: function(data) {
|
||||
history_table.ajax.reload(null, false);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
@@ -74,7 +74,7 @@ history_table_modal_options = {
|
||||
{
|
||||
"targets": [3],
|
||||
"data":"player",
|
||||
"className": "modal-control no-wrap hidden-sm hidden-xs"
|
||||
"className": "no-wrap hidden-sm hidden-xs modal-control"
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
|
@@ -24,13 +24,11 @@ user_ip_table_options = {
|
||||
},
|
||||
"searchable": false,
|
||||
"width": "15%",
|
||||
"className": "no-wrap"
|
||||
"className": "no-wrap hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
"data":"ip_address",
|
||||
"width": "15%",
|
||||
"className": "modal-control no-wrap",
|
||||
"data": "ip_address",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData) {
|
||||
if (isPrivateIP(cellData)) {
|
||||
@@ -46,31 +44,83 @@ user_ip_table_options = {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "15%"
|
||||
"width": "15%",
|
||||
"className": "no-wrap modal-control-ip"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
"data":"play_count",
|
||||
"width": "10%",
|
||||
"className": "hidden-xs"
|
||||
"data":"platform",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData) {
|
||||
var transcode_dec = '';
|
||||
if (rowData['video_decision'] === 'transcode') {
|
||||
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span> ';
|
||||
} 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> ';
|
||||
} 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> ';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + cellData + '</div></a></div>');
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
},
|
||||
"width": "15%",
|
||||
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"data":"platform",
|
||||
"width": "15%",
|
||||
"className": "hidden-xs"
|
||||
"data":"last_watched",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData !== '') {
|
||||
var media_type = '';
|
||||
var thumb_popover = ''
|
||||
if (rowData['media_type'] === 'movie') {
|
||||
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
|
||||
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
|
||||
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + ' ' + 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 + ' ' + 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 + ' ' + thumb_popover + '</div></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
}
|
||||
},
|
||||
"className": "hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [4],
|
||||
"data":"last_watched",
|
||||
"width": "30%",
|
||||
"className": "hidden-sm hidden-xs"
|
||||
"data":"play_count",
|
||||
"searchable": false,
|
||||
"width": "10%"
|
||||
}
|
||||
],
|
||||
"drawCallback": function (settings) {
|
||||
// Jump to top of page
|
||||
// $('html,body').scrollTop(0);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
|
||||
// Create the tooltips.
|
||||
$('.transcode-tooltip').tooltip();
|
||||
$('.media-type-tooltip').tooltip();
|
||||
$('.watched-tooltip').tooltip();
|
||||
$('.thumb-tooltip').popover({
|
||||
html: true,
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
content: function () {
|
||||
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
"preDrawCallback": function(settings) {
|
||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
||||
@@ -83,6 +133,25 @@ $('#user_ip_table').on('mouseenter', 'td.modal-control span', function () {
|
||||
});
|
||||
|
||||
$('#user_ip_table').on('click', 'td.modal-control', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = user_ip_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
function showStreamDetails() {
|
||||
$.ajax({
|
||||
url: 'get_stream_data',
|
||||
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
$("#info-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
showStreamDetails();
|
||||
});
|
||||
|
||||
$('#user_ip_table').on('click', 'td.modal-control-ip', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = user_ip_table.row( tr );
|
||||
var rowData = row.data();
|
||||
|
@@ -18,7 +18,7 @@ users_list_table_options = {
|
||||
"columnDefs": [
|
||||
{
|
||||
"targets": [0],
|
||||
"data": "thumb",
|
||||
"data": "user_thumb",
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData === '') {
|
||||
$(td).html('<img src="interfaces/default/images/gravatar-default-80x80.png" alt="User Logo"/>');
|
||||
@@ -28,8 +28,8 @@ users_list_table_options = {
|
||||
},
|
||||
"orderable": false,
|
||||
"searchable": false,
|
||||
"className": "users-poster-face",
|
||||
"width": "40px"
|
||||
"width": "5%",
|
||||
"className": "users-poster-face"
|
||||
},
|
||||
{
|
||||
"targets": [1],
|
||||
@@ -45,6 +45,7 @@ users_list_table_options = {
|
||||
$(td).html(cellData);
|
||||
}
|
||||
},
|
||||
"width": "15%"
|
||||
},
|
||||
{
|
||||
"targets": [2],
|
||||
@@ -57,24 +58,84 @@ users_list_table_options = {
|
||||
}
|
||||
},
|
||||
"searchable": false,
|
||||
"className": "hidden-xs",
|
||||
"width": "15%",
|
||||
"className": "no-wrap hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [3],
|
||||
"data": "ip_address",
|
||||
"render": function ( data, type, full ) {
|
||||
if (data) {
|
||||
return data;
|
||||
"createdCell": function (td, cellData, rowData, row, col) {
|
||||
if (cellData) {
|
||||
if (isPrivateIP(cellData)) {
|
||||
if (cellData != '') {
|
||||
$(td).html(cellData);
|
||||
} else {
|
||||
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> ' + 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],
|
||||
"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> ';
|
||||
} 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> ';
|
||||
} 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> ';
|
||||
}
|
||||
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + ' ' + 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 + ' ' + 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 + ' ' + 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 + ' ' + thumb_popover + '</div></div>');
|
||||
} else if (rowData['media_type']) {
|
||||
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
|
||||
} else {
|
||||
$(td).html('n/a');
|
||||
}
|
||||
}
|
||||
},
|
||||
"className": "hidden-sm hidden-xs"
|
||||
},
|
||||
{
|
||||
"targets": [6],
|
||||
"data": "plays",
|
||||
"searchable": false
|
||||
"searchable": false,
|
||||
"width": "10%"
|
||||
}
|
||||
|
||||
],
|
||||
@@ -82,9 +143,65 @@ users_list_table_options = {
|
||||
// Jump to top of page
|
||||
//$('html,body').scrollTop(0);
|
||||
$('#ajaxMsg').fadeOut();
|
||||
|
||||
// Create the tooltips.
|
||||
$('.transcode-tooltip').tooltip();
|
||||
$('.media-type-tooltip').tooltip();
|
||||
$('.watched-tooltip').tooltip();
|
||||
$('.thumb-tooltip').popover({
|
||||
html: true,
|
||||
trigger: 'hover',
|
||||
placement: 'right',
|
||||
content: function () {
|
||||
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
"preDrawCallback": function(settings) {
|
||||
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i> Fetching rows...</div>";
|
||||
showMsg(msg, false, false, 0)
|
||||
}
|
||||
}
|
||||
|
||||
$('#users_list_table').on('click', 'td.modal-control', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = users_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
function showStreamDetails() {
|
||||
$.ajax({
|
||||
url: 'get_stream_data',
|
||||
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
|
||||
cache: false,
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
$("#info-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
showStreamDetails();
|
||||
});
|
||||
|
||||
$('#users_list_table').on('click', 'td.modal-control-ip', function () {
|
||||
var tr = $(this).parents('tr');
|
||||
var row = users_list_table.row(tr);
|
||||
var rowData = row.data();
|
||||
|
||||
function getUserLocation(ip_address) {
|
||||
if (isPrivateIP(ip_address)) {
|
||||
return "n/a"
|
||||
} else {
|
||||
$.ajax({
|
||||
url: 'get_ip_address_details',
|
||||
data: { ip_address: ip_address },
|
||||
async: true,
|
||||
complete: function (xhr, status) {
|
||||
$("#ip-info-modal").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getUserLocation(rowData['ip_address']);
|
||||
});
|
@@ -86,6 +86,7 @@ from plexpy import helpers
|
||||
|
||||
$(document).ready(function() {
|
||||
LoadPlexPyLogs();
|
||||
clearSearchButton('log_table', log_table);
|
||||
});
|
||||
|
||||
function LoadPlexPyLogs() {
|
||||
@@ -105,11 +106,13 @@ from plexpy import helpers
|
||||
$("#plexpy-logs-btn").click(function() {
|
||||
$("#clear-logs").show();
|
||||
LoadPlexPyLogs();
|
||||
clearSearchButton('log_table', log_table);
|
||||
});
|
||||
|
||||
$("#plex-logs-btn").click(function() {
|
||||
$("#clear-logs").hide();
|
||||
LoadPlexLogs();
|
||||
clearSearchButton('plex_log_table', plex_log_table);
|
||||
});
|
||||
|
||||
$("#clear-logs").click(function() {
|
||||
|
@@ -15,6 +15,7 @@ type Returns the type of media. Either 'movie' or 'season'.
|
||||
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
|
||||
added_at Returns the time when the media was added to the library.
|
||||
title Returns the name of the movie or season.
|
||||
parent_title Returns the name of the TV Show a season belongs too.
|
||||
|
||||
== Only if 'type' is 'movie' ==
|
||||
year Returns the movie release year.
|
||||
@@ -28,27 +29,32 @@ DOCUMENTATION :: END
|
||||
% for item in data:
|
||||
<div class="dashboard-recent-media-instance">
|
||||
<li>
|
||||
<div class="poster">
|
||||
% if item['type'] == 'season' or item['type'] == 'movie':
|
||||
<div class="poster-face">
|
||||
<a href="info?item_id=${item['rating_key']}">
|
||||
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
|
||||
</a>
|
||||
</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 class="poster">
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% if item['type'] == 'season' or item['type'] == 'album':
|
||||
<h3>${item['title']}</h3>
|
||||
% if item['type'] == 'season':
|
||||
<h3>${item['parent_title']}</h3>
|
||||
<h3>(${item['title']})</h3>
|
||||
% elif item['type'] == 'movie':
|
||||
<h3>${item['title']} (${item['year']})</h3>
|
||||
<h3>${item['title']}</h3>
|
||||
<h3>(${item['year']})</h3>
|
||||
% endif
|
||||
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
|
||||
</div>
|
||||
</a>
|
||||
% elif item['type'] == 'album':
|
||||
<div class="poster">
|
||||
<div class="cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"></div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
<h3>${item['parent_title']}</h3>
|
||||
<h3>${item['title']}</h3>
|
||||
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
|
||||
</div>
|
||||
% endif
|
||||
</li>
|
||||
</div>
|
||||
<script>
|
||||
|
@@ -1,7 +1,7 @@
|
||||
<%inherit file="base.html"/>
|
||||
<%!
|
||||
import plexpy
|
||||
from plexpy import notifiers
|
||||
from plexpy import notifiers, common, versioncheck
|
||||
|
||||
available_notification_agents = notifiers.available_notification_agents()
|
||||
%>
|
||||
@@ -48,9 +48,11 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="tabs-1">
|
||||
% if common.VERSION_NUMBER:
|
||||
<div class="padded-header">
|
||||
<h3>Software Updates</h3>
|
||||
<h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
|
||||
</div>
|
||||
% endif
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
|
||||
@@ -58,7 +60,7 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
|
||||
</div>
|
||||
% if plexpy.CURRENT_VERSION:
|
||||
<p>Current version: ${plexpy.CURRENT_VERSION}</p>
|
||||
<p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
|
||||
% endif
|
||||
<div class="padded-header">
|
||||
<h3>Display Settings</h3>
|
||||
@@ -276,8 +278,12 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
<p class="help-block">If you have media indexing enabled on your server, use these on the activity pane.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Homepage Statistics</h3>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="home_stats_length">Homepage Statistics Time Frame</label>
|
||||
<label for="home_stats_length">Time Frame</label>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="home_stats_length" name="home_stats_length" value="${config['home_stats_length']}" size="3" data-parsley-min="0" data-parsley-trigger="change" required>
|
||||
@@ -285,6 +291,12 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
</div>
|
||||
<p class="help-block">Specify the number of days for the statistics on the home page. Default is 30 days.</p>
|
||||
</div>
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" id="home_stats_type" name="home_stats_type" value="1" ${config['home_stats_type']}> Use play duration
|
||||
</label>
|
||||
<p class="help-block">Use play duration instead of play count to generate statistics.</p>
|
||||
</div>
|
||||
|
||||
<div class="padded-header">
|
||||
<h3>Plex Logs</h3>
|
||||
@@ -369,10 +381,10 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
<label for="buffer_threshold">Buffer Threshold</label>
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="buffer_threshold" name="buffer_threshold" value="${config['buffer_threshold']}" data-parsley-range="[1,50]" data-parsley-trigger="change" required>
|
||||
<input type="text" class="form-control" data-parsley-type="integer" id="buffer_threshold" name="buffer_threshold" value="${config['buffer_threshold']}" data-parsley-range="[0,50]" data-parsley-trigger="change" required>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">How many buffer events should we wait before triggering the first warning. Buffer events increment on each monitor ping if play state is buffering.</p>
|
||||
<p class="help-block">How many buffer events should we wait before triggering the first warning. Buffer events increment on each monitor ping if play state is buffering. 0 to disable buffer warnings.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="buffer_wait">Buffer Wait</label>
|
||||
@@ -810,10 +822,18 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
<td width="150"><strong>{season_num}</strong></td>
|
||||
<td>The season number for the media item if item is episode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="150"><strong>{season_num00}</strong></td>
|
||||
<td>The two digit season number.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="150"><strong>{episode_num}</strong></td>
|
||||
<td>The episode number for the media item if item is episode.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="150"><strong>{episode_num00}</strong></td>
|
||||
<td>The two digit episode number.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="150"><strong>{rating}</strong></td>
|
||||
<td>The rating (out of 10) for the item.</td>
|
||||
@@ -822,6 +842,10 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
<td width="150"><strong>{duration}</strong></td>
|
||||
<td>The duration (in minutes) for the item.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="150"><strong>{stream_duration}</strong></td>
|
||||
<td>The stream duration (in minutes) for the item.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="150"><strong>{progress}</strong></td>
|
||||
<td>The last reported offset (in minutes) for the item.</td>
|
||||
@@ -875,6 +899,21 @@ available_notification_agents = notifiers.available_notification_agents()
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="changelog-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
|
||||
<h4 class="modal-title">Changelog</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
${versioncheck.read_changelog()}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</%def>
|
||||
|
||||
@@ -1023,7 +1062,9 @@ $(document).ready(function() {
|
||||
headers: {'Content-Type': 'application/xml; charset=utf-8',
|
||||
'X-Plex-Device-Name': 'PlexPy',
|
||||
'X-Plex-Product': 'PlexPy',
|
||||
'X-Plex-Version': 'v0.1 dev',
|
||||
'X-Plex-Version': '${common.VERSION_NUMBER}',
|
||||
'X-Plex-Platform': '${common.PLATFORM}',
|
||||
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
|
||||
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
|
||||
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
|
||||
},
|
||||
|
@@ -55,7 +55,7 @@ DOCUMENTATION :: END
|
||||
<div class="col-md-4">
|
||||
<h4><strong>Stream Details</strong></h4>
|
||||
<h5>Video</h5>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
% if data['transcode_video_dec'] != 'direct play':
|
||||
<li>Stream Type: <strong>${data['transcode_video_dec']}</strong></li>
|
||||
<li>Video Resolution: <strong>${data['transcode_height']}p</strong></li>
|
||||
@@ -75,7 +75,7 @@ DOCUMENTATION :: END
|
||||
% endif
|
||||
</ul>
|
||||
<h5>Audio</h5>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
% if data['transcode_audio_dec'] != 'direct play':
|
||||
<li>Stream Type: <strong>${data['transcode_audio_dec']}</strong></li>
|
||||
<li>Audio Codec: <strong>${data['transcode_audio_codec']}</strong></li>
|
||||
@@ -89,7 +89,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4><strong>Media Source Details</strong></h4>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li>Container: <strong>${data['container']}</strong></li>
|
||||
<li>Resolution: <strong>${data['height']}p</strong></li>
|
||||
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
|
||||
@@ -97,7 +97,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<h4><strong>Video Source Details</strong></h4>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li>Width: <strong>${data['width']}</strong></li>
|
||||
<li>Height: <strong>${data['height']}</strong></li>
|
||||
<li>Aspect Ratio: <strong>${data['aspect_ratio']}</strong></li>
|
||||
@@ -105,7 +105,7 @@ DOCUMENTATION :: END
|
||||
<li>Video Codec: <strong>${data['video_codec']}</strong></li>
|
||||
</ul>
|
||||
<h4><strong>Audio Source Details</strong></h4>
|
||||
<ul>
|
||||
<ul class="list-unstyled">
|
||||
<li>Audio Codec: <strong>${data['audio_codec']}</strong></li>
|
||||
<li>Audio Channels: <strong>${data['audio_channels']}</strong></li>
|
||||
</ul>
|
||||
|
@@ -57,9 +57,10 @@
|
||||
"url": "get_sync"
|
||||
}
|
||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
|
||||
var colvis = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis.button() ).appendTo('div.colvis-button-bar');
|
||||
|
||||
clearSearchButton('sync_table', sync_table);
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
@@ -103,7 +103,7 @@ from plexpy import helpers
|
||||
<div class="col-md-12">
|
||||
<div class="table-card-header">
|
||||
<div class="header-bar">
|
||||
<span><i class="fa fa-history"></i> Recently watched</span>
|
||||
<span><i class="fa fa-history"></i> Recently Watched</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
@@ -131,17 +131,15 @@ from plexpy import helpers
|
||||
<table id="user_ip_table" class="display" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align="left">Last seen</th>
|
||||
<th align="left">Last Seen</th>
|
||||
<th align="left">IP Address</th>
|
||||
<th align="left">Play Count</th>
|
||||
<th align="left">Platform (Last Seen)</th>
|
||||
<th align="left">Last Platform</th>
|
||||
<th align="left">Last Watched</th>
|
||||
<th align="left">Play Count</th>
|
||||
</tr>
|
||||
</thead>
|
||||
</table>
|
||||
</div>
|
||||
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,13 +154,17 @@ from plexpy import helpers
|
||||
<span class="set-username">${data['friendly_name']}</span>
|
||||
</strong></span>
|
||||
</div>
|
||||
<div class="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> 
|
||||
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-card-back">
|
||||
<table class="display" id="history_table" width="100%">
|
||||
<thead>
|
||||
<tr>
|
||||
<th align='left' id="delete">Delete</th>
|
||||
<th align='left' id="time">Time</th>
|
||||
<th align='left' id="friendly_name">User</th>
|
||||
<th align='left' id="platform">Platform</th>
|
||||
@@ -172,14 +174,13 @@ from plexpy import helpers
|
||||
<th align='left' id="paused_counter">Paused</th>
|
||||
<th align='left' id="stopped">Stopped</th>
|
||||
<th align='left' id="duration">Duration</th>
|
||||
<th align='left' id="percent_complete">Watched</th>
|
||||
<th align='left' id="percent_complete"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -222,6 +223,10 @@ from plexpy import helpers
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||
</div>
|
||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||
</div>
|
||||
</div>
|
||||
<footer></footer>
|
||||
</%def>
|
||||
@@ -236,6 +241,33 @@ from plexpy import helpers
|
||||
<script src="interfaces/default/js/tables/user_ips.js"></script>
|
||||
<script src="interfaces/default/js/tables/sync_table.js"></script>
|
||||
<script>
|
||||
function recentlyWatched() {
|
||||
var widthVal = $('body').find("#user-recently-watched").width();
|
||||
var tmp = (widthVal-32) / 180;
|
||||
|
||||
if (tmp > 0) {
|
||||
containerSize = parseInt(tmp);
|
||||
} else {
|
||||
containerSize = 1;
|
||||
}
|
||||
|
||||
% if data['user_id']:
|
||||
var user_id = ${data['user_id']};
|
||||
% else:
|
||||
var user_id = null;
|
||||
% endif
|
||||
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: { user_id: user_id, user: '${data['username']}', limit: containerSize },
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$(document).ready(function () {
|
||||
|
||||
% if data['user_id']:
|
||||
@@ -266,16 +298,6 @@ from plexpy import helpers
|
||||
}
|
||||
});
|
||||
|
||||
// Populate recently watched
|
||||
$.ajax({
|
||||
url: 'get_user_recently_watched',
|
||||
async: true,
|
||||
data: { user_id: user_id, user: '${data['username']}' },
|
||||
complete: function(xhr, status) {
|
||||
$("#user-recently-watched").html(xhr.responseText);
|
||||
}
|
||||
});
|
||||
|
||||
$( "#history-tab-btn" ).one( "click", function() {
|
||||
// Build watch history table
|
||||
history_table_options.ajax = {
|
||||
@@ -289,10 +311,12 @@ from plexpy import helpers
|
||||
}
|
||||
}
|
||||
history_table = $('#history_table').DataTable(history_table_options);
|
||||
history_table.column(1).visible(false);
|
||||
history_table.column(2).visible(false);
|
||||
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
|
||||
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
|
||||
$(colvis.button()).appendTo('#button-bar-history');
|
||||
|
||||
clearSearchButton('history_table', history_table);
|
||||
});
|
||||
|
||||
$( "#ip-tab-btn" ).one( "click", function() {
|
||||
@@ -308,6 +332,8 @@ from plexpy import helpers
|
||||
}
|
||||
}
|
||||
user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
|
||||
|
||||
clearSearchButton('user_ip_table', user_ip_table);
|
||||
});
|
||||
|
||||
$( "#sync-tab-btn" ).one( "click", function() {
|
||||
@@ -320,10 +346,12 @@ from plexpy import helpers
|
||||
}
|
||||
}
|
||||
sync_table = $('#sync_table').DataTable(sync_table_options);
|
||||
history_table.column(1).visible(false);
|
||||
sync_table.column(1).visible(false);
|
||||
|
||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
|
||||
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
|
||||
$( colvis_sync.button() ).appendTo('#button-bar-sync');
|
||||
|
||||
clearSearchButton('sync_table', sync_table);
|
||||
});
|
||||
|
||||
// Load edit user modal
|
||||
@@ -339,6 +367,24 @@ from plexpy import helpers
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Delete mode button
|
||||
$('#row-edit-mode').click(function() {
|
||||
if ($(this).hasClass('active')) {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).addClass('hidden');
|
||||
});
|
||||
} else {
|
||||
$('.delete-control').each(function() {
|
||||
$(this).removeClass('hidden');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
recentlyWatched();
|
||||
$(window).resize(function() {
|
||||
recentlyWatched();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
|
@@ -34,7 +34,7 @@ DOCUMENTATION :: END
|
||||
</div>
|
||||
</ul>
|
||||
<script>
|
||||
$("#user-platform-image-${a['result_id']}").html("<img class='user-platforms-instance-poster' src='" + getPlatformImagePath('${a['platform_type']}') + "'>");
|
||||
$("#user-platform-image-${a['result_id']}").html("<div class='user-platforms-instance-poster' style='background-image: url(" + getPlatformImagePath('${a['platform_type']}') + ");'>");
|
||||
</script>
|
||||
% endfor
|
||||
% else:
|
||||
|
@@ -18,6 +18,7 @@ time Returns the last watched time of the media.
|
||||
title Returns the name of the movie or episode.
|
||||
|
||||
== Only if 'type' is 'episode ==
|
||||
parent_title Returns the name of the TV Show a season belongs too.
|
||||
parent_index Returns the season number.
|
||||
index Returns the episode number.
|
||||
|
||||
@@ -33,21 +34,22 @@ DOCUMENTATION :: END
|
||||
% for item in data:
|
||||
<div class="dashboard-recent-media-instance">
|
||||
<li>
|
||||
<div class="poster">
|
||||
<div class="poster-face">
|
||||
<a href="info?source=history&item_id=${item['row_id']}">
|
||||
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
|
||||
</a>
|
||||
</div>
|
||||
<div class="poster">
|
||||
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
|
||||
</div>
|
||||
<div class="dashboard-recent-media-metacontainer">
|
||||
% 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':
|
||||
<h3>${item['title']} (${item['year']})</h3>
|
||||
<h3>${item['title']}</h3>
|
||||
<h3>(${item['year']})</h3>
|
||||
% endif
|
||||
<div class="text-muted" id="time-${item['time']}">${item['time']}</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
</div>
|
||||
<script>
|
||||
|
@@ -30,8 +30,8 @@ DOCUMENTATION :: END
|
||||
<h4>Last ${a['query_days']} days</h4>
|
||||
% endif
|
||||
<h3>${a['total_plays']}</h3>
|
||||
|
||||
<p>plays</p>
|
||||
<h3><strong>/</strong></h3>
|
||||
<span id="total-time-${a['query_days']}"></span>
|
||||
</div>
|
||||
</li>
|
||||
|
@@ -23,12 +23,18 @@
|
||||
<th align="left" id="friendly_name">User</th>
|
||||
<th align="left" id="last_seen">Last Seen</th>
|
||||
<th align="left" id="last_known_ip">Last Known IP</th>
|
||||
<th align="left" id="last_platform">Last Platform</th>
|
||||
<th align="left" id="last_watched">Last Watched</th>
|
||||
<th align="left" id="total_plays">Total Plays</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
|
||||
</div>
|
||||
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -41,6 +47,7 @@
|
||||
<script src="interfaces/default/js/moment-with-locale.js"></script>
|
||||
<script src="interfaces/default/js/tables/users.js"></script>
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
users_list_table_options.ajax = {
|
||||
"url": "get_user_list",
|
||||
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() {
|
||||
$.ajax({
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<%
|
||||
import plexpy
|
||||
from plexpy import version
|
||||
from plexpy import common
|
||||
%>
|
||||
|
||||
<!doctype html>
|
||||
@@ -355,7 +355,9 @@ from plexpy import version
|
||||
headers: {'Content-Type': 'application/xml; charset=utf-8',
|
||||
'X-Plex-Device-Name': 'PlexPy',
|
||||
'X-Plex-Product': 'PlexPy',
|
||||
'X-Plex-Version': 'v0.1 dev',
|
||||
'X-Plex-Version': '${common.VERSION_NUMBER}',
|
||||
'X-Plex-Platform': '${common.PLATFORM}',
|
||||
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
|
||||
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
|
||||
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
|
||||
},
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# PlexPy - Automatic music downloader for SABnzbd
|
||||
# PlexPy
|
||||
#
|
||||
# Service Unit file for systemd system manager
|
||||
#
|
||||
@@ -53,7 +53,7 @@
|
||||
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
|
||||
|
||||
[Unit]
|
||||
Description=PlexPy - Automatic music downloader for SABnzbd
|
||||
Description=PlexPy
|
||||
|
||||
[Service]
|
||||
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet
|
||||
|
80
init-scripts/init.freenas
Normal file
80
init-scripts/init.freenas
Normal 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"
|
@@ -1,4 +1,4 @@
|
||||
# plexpy - Automatic music downloader
|
||||
# plexpy
|
||||
#
|
||||
# This is a session/user job. Install this file into /usr/share/upstart/sessions
|
||||
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if
|
||||
|
@@ -388,7 +388,7 @@ def dbcheck():
|
||||
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL UNIQUE, '
|
||||
'friendly_name TEXT, thumb TEXT, email TEXT, is_home_user INTEGER DEFAULT NULL, '
|
||||
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
|
||||
'keep_history INTEGER DEFAULT 1)'
|
||||
'keep_history INTEGER DEFAULT 1, custom_avatar_url TEXT)'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
@@ -534,29 +534,29 @@ def dbcheck():
|
||||
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER)'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT do_notify from users')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
logger.debug(u"Altering database. Updating database table users.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE users ADD COLUMN do_notify INTEGER DEFAULT 1'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT keep_history from users')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
logger.debug(u"Altering database. Updating database table users.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE users ADD COLUMN keep_history INTEGER DEFAULT 1'
|
||||
)
|
||||
|
||||
# Upgrade sessions table from earlier versions
|
||||
# Upgrade notify_log table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT on_pause from notify_log')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table sessions.")
|
||||
logger.debug(u"Altering database. Updating database table notify_log.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE notify_log ADD COLUMN on_pause INTEGER'
|
||||
)
|
||||
@@ -579,6 +579,15 @@ def dbcheck():
|
||||
'ALTER TABLE sessions ADD COLUMN buffer_last_triggered INTEGER'
|
||||
)
|
||||
|
||||
# Upgrade users table from earlier versions
|
||||
try:
|
||||
c_db.execute('SELECT custom_avatar_url from users')
|
||||
except sqlite3.OperationalError:
|
||||
logger.debug(u"Altering database. Updating database table users.")
|
||||
c_db.execute(
|
||||
'ALTER TABLE users ADD COLUMN custom_avatar_url TEXT'
|
||||
)
|
||||
|
||||
conn_db.commit()
|
||||
c_db.close()
|
||||
|
||||
|
@@ -19,14 +19,17 @@ Created on Aug 1, 2011
|
||||
@author: Michael
|
||||
'''
|
||||
import platform
|
||||
import operator
|
||||
import os
|
||||
import re
|
||||
|
||||
from plexpy import version
|
||||
|
||||
# Identify Our Application
|
||||
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' (' + platform.system() + ' ' + platform.release() + ')'
|
||||
USER_AGENT = 'PlexPy/-' + version.PLEXPY_VERSION + ' v' + version.PLEXPY_RELEASE_VERSION + ' (' + platform.system() + \
|
||||
' ' + platform.release() + ')'
|
||||
|
||||
PLATFORM = platform.system()
|
||||
PLATFORM_VERSION = platform.release()
|
||||
BRANCH = version.PLEXPY_VERSION
|
||||
VERSION_NUMBER = version.PLEXPY_RELEASE_VERSION
|
||||
|
||||
# Notification Types
|
||||
NOTIFY_STARTED = 1
|
||||
|
@@ -83,6 +83,7 @@ _CONFIG_DEFINITIONS = {
|
||||
'GROWL_ON_BUFFER': (int, 'Growl', 0),
|
||||
'GROWL_ON_WATCHED': (int, 'Growl', 0),
|
||||
'HOME_STATS_LENGTH': (int, 'General', 30),
|
||||
'HOME_STATS_TYPE': (int, 'General', 0),
|
||||
'HTTPS_CERT': (str, 'General', ''),
|
||||
'HTTPS_KEY': (str, 'General', ''),
|
||||
'HTTP_HOST': (str, 'General', '0.0.0.0'),
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# This file is part of PlexPy.
|
||||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -26,63 +26,6 @@ class DataFactory(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def get_user_list(self, kwargs=None):
|
||||
data_tables = datatables.DataTables()
|
||||
|
||||
columns = ['users.user_id as user_id',
|
||||
'users.thumb 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):
|
||||
data_tables = datatables.DataTables()
|
||||
|
||||
@@ -93,6 +36,13 @@ class DataFactory(object):
|
||||
'session_history.player',
|
||||
'session_history.ip_address',
|
||||
'session_history_metadata.full_title as full_title',
|
||||
'session_history_metadata.thumb',
|
||||
'session_history_metadata.parent_thumb',
|
||||
'session_history_metadata.grandparent_thumb',
|
||||
'session_history_metadata.media_index',
|
||||
'session_history_metadata.parent_media_index',
|
||||
'session_history_metadata.parent_title',
|
||||
'session_history_metadata.year',
|
||||
'session_history.started',
|
||||
'session_history.paused_counter',
|
||||
'session_history.stopped',
|
||||
@@ -138,12 +88,24 @@ class DataFactory(object):
|
||||
|
||||
rows = []
|
||||
for item in history:
|
||||
if item["media_type"] == 'episode' and item["parent_thumb"]:
|
||||
thumb = item["parent_thumb"]
|
||||
elif item["media_type"] == 'episode':
|
||||
thumb = item["grandparent_thumb"]
|
||||
else:
|
||||
thumb = item["thumb"]
|
||||
|
||||
row = {"id": item['id'],
|
||||
"date": item['date'],
|
||||
"friendly_name": item['friendly_name'],
|
||||
"player": item["player"],
|
||||
"ip_address": item["ip_address"],
|
||||
"full_title": item["full_title"],
|
||||
"thumb": thumb,
|
||||
"media_index": item["media_index"],
|
||||
"parent_media_index": item["parent_media_index"],
|
||||
"parent_title": item["parent_title"],
|
||||
"year": item["year"],
|
||||
"started": item["started"],
|
||||
"paused_counter": item["paused_counter"],
|
||||
"stopped": item["stopped"],
|
||||
@@ -167,312 +129,16 @@ class DataFactory(object):
|
||||
|
||||
return dict
|
||||
|
||||
def get_user_unique_ips(self, kwargs=None, custom_where=None):
|
||||
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 = {"thumb": 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 = {"thumb": 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, 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, 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, ' \
|
||||
'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, ' \
|
||||
'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, ' \
|
||||
'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, ' \
|
||||
'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'):
|
||||
def get_home_stats(self, time_range='30', stat_type='0'):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
if not time_range.isdigit():
|
||||
time_range = '30'
|
||||
|
||||
stats_queries = ["top_tv", "popular_tv", "top_users", "top_platforms"]
|
||||
sort_type = 'total_plays' if stat_type == '0' else 'total_duration'
|
||||
|
||||
# This actually determines the output order in the home page
|
||||
stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms"]
|
||||
home_stats = []
|
||||
|
||||
for stat in stats_queries:
|
||||
@@ -482,6 +148,10 @@ class DataFactory(object):
|
||||
query = 'SELECT session_history_metadata.id, ' \
|
||||
'session_history_metadata.grandparent_title, ' \
|
||||
'COUNT(session_history_metadata.grandparent_title) as total_plays, ' \
|
||||
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
|
||||
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
|
||||
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
|
||||
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
|
||||
'session_history_metadata.grandparent_rating_key, ' \
|
||||
'MAX(session_history.started) as last_watch,' \
|
||||
'session_history_metadata.grandparent_thumb ' \
|
||||
@@ -491,7 +161,7 @@ class DataFactory(object):
|
||||
'>= datetime("now", "-%s days", "localtime") ' \
|
||||
'AND session_history_metadata.media_type = "episode" ' \
|
||||
'GROUP BY session_history_metadata.grandparent_title ' \
|
||||
'ORDER BY total_plays DESC LIMIT 10' % time_range
|
||||
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
|
||||
result = monitor_db.select(query)
|
||||
except:
|
||||
logger.warn("Unable to execute database query.")
|
||||
@@ -500,10 +170,11 @@ class DataFactory(object):
|
||||
for item in result:
|
||||
row = {'title': item[1],
|
||||
'total_plays': item[2],
|
||||
'total_duration': item[3],
|
||||
'users_watched': '',
|
||||
'rating_key': item[3],
|
||||
'last_play': item[4],
|
||||
'grandparent_thumb': item[5],
|
||||
'rating_key': item[4],
|
||||
'last_play': item[5],
|
||||
'grandparent_thumb': item[6],
|
||||
'thumb': '',
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
@@ -514,8 +185,55 @@ class DataFactory(object):
|
||||
top_tv.append(row)
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
'stat_type': sort_type,
|
||||
'rows': top_tv})
|
||||
|
||||
elif 'top_movies' in stat:
|
||||
top_movies = []
|
||||
try:
|
||||
query = 'SELECT session_history_metadata.id, ' \
|
||||
'session_history_metadata.full_title, ' \
|
||||
'COUNT(session_history_metadata.full_title) as total_plays, ' \
|
||||
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
|
||||
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
|
||||
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
|
||||
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
|
||||
'session_history_metadata.rating_key, ' \
|
||||
'MAX(session_history.started) as last_watch,' \
|
||||
'session_history_metadata.thumb ' \
|
||||
'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 %s DESC LIMIT 10' % (time_range, sort_type)
|
||||
result = monitor_db.select(query)
|
||||
except:
|
||||
logger.warn("Unable to execute database query.")
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
row = {'title': item[1],
|
||||
'total_plays': item[2],
|
||||
'total_duration': item[3],
|
||||
'users_watched': '',
|
||||
'rating_key': item[4],
|
||||
'last_play': item[5],
|
||||
'grandparent_thumb': '',
|
||||
'thumb': item[6],
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform_type': '',
|
||||
'platform': '',
|
||||
'row_id': item[0]
|
||||
}
|
||||
top_movies.append(row)
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
'stat_type': sort_type,
|
||||
'rows': top_movies})
|
||||
|
||||
elif 'popular_tv' in stat:
|
||||
popular_tv = []
|
||||
try:
|
||||
@@ -558,6 +276,48 @@ class DataFactory(object):
|
||||
home_stats.append({'stat_id': stat,
|
||||
'rows': popular_tv})
|
||||
|
||||
elif 'popular_movies' in stat:
|
||||
popular_movies = []
|
||||
try:
|
||||
query = 'SELECT session_history_metadata.id, ' \
|
||||
'session_history_metadata.full_title, ' \
|
||||
'COUNT(DISTINCT session_history.user_id) as users_watched, ' \
|
||||
'session_history_metadata.rating_key, ' \
|
||||
'MAX(session_history.started) as last_watch, ' \
|
||||
'COUNT(session_history.id) as total_plays, ' \
|
||||
'session_history_metadata.thumb ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
|
||||
'>= datetime("now", "-%s days", "localtime") ' \
|
||||
'AND session_history_metadata.media_type = "movie" ' \
|
||||
'GROUP BY session_history_metadata.full_title ' \
|
||||
'ORDER BY users_watched DESC, total_plays DESC ' \
|
||||
'LIMIT 10' % time_range
|
||||
result = monitor_db.select(query)
|
||||
except:
|
||||
logger.warn("Unable to execute database query.")
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
row = {'title': item[1],
|
||||
'users_watched': item[2],
|
||||
'rating_key': item[3],
|
||||
'last_play': item[4],
|
||||
'total_plays': item[5],
|
||||
'grandparent_thumb': '',
|
||||
'thumb': item[6],
|
||||
'user': '',
|
||||
'friendly_name': '',
|
||||
'platform_type': '',
|
||||
'platform': '',
|
||||
'row_id': item[0]
|
||||
}
|
||||
popular_movies.append(row)
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
'rows': popular_movies})
|
||||
|
||||
elif 'top_users' in stat:
|
||||
top_users = []
|
||||
try:
|
||||
@@ -565,8 +325,12 @@ class DataFactory(object):
|
||||
'(case when users.friendly_name is null then session_history.user else ' \
|
||||
'users.friendly_name end) as friendly_name,' \
|
||||
'COUNT(session_history.id) as total_plays, ' \
|
||||
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
|
||||
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
|
||||
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
|
||||
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
|
||||
'MAX(session_history.started) as last_watch, ' \
|
||||
'users.thumb, ' \
|
||||
'users.custom_avatar_url as thumb, ' \
|
||||
'users.user_id ' \
|
||||
'FROM session_history ' \
|
||||
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
|
||||
@@ -574,23 +338,24 @@ class DataFactory(object):
|
||||
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
|
||||
'datetime("now", "-%s days", "localtime") '\
|
||||
'GROUP BY session_history.user_id ' \
|
||||
'ORDER BY total_plays DESC LIMIT 10' % time_range
|
||||
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
|
||||
result = monitor_db.select(query)
|
||||
except:
|
||||
logger.warn("Unable to execute database query.")
|
||||
return None
|
||||
|
||||
for item in result:
|
||||
if not item[4] or item[4] == '':
|
||||
if not item[5] or item[5] == '':
|
||||
user_thumb = common.DEFAULT_USER_THUMB
|
||||
else:
|
||||
user_thumb = item[4]
|
||||
user_thumb = item[5]
|
||||
|
||||
row = {'user': item[0],
|
||||
'user_id': item[5],
|
||||
'user_id': item[6],
|
||||
'friendly_name': item[1],
|
||||
'total_plays': item[2],
|
||||
'last_play': item[3],
|
||||
'total_duration': item[3],
|
||||
'last_play': item[4],
|
||||
'thumb': user_thumb,
|
||||
'grandparent_thumb': '',
|
||||
'users_watched': '',
|
||||
@@ -603,6 +368,7 @@ class DataFactory(object):
|
||||
top_users.append(row)
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
'stat_type': sort_type,
|
||||
'rows': top_users})
|
||||
|
||||
elif 'top_platforms' in stat:
|
||||
@@ -611,6 +377,10 @@ class DataFactory(object):
|
||||
try:
|
||||
query = 'SELECT session_history.platform, ' \
|
||||
'COUNT(session_history.id) as total_plays, ' \
|
||||
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
|
||||
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
|
||||
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
|
||||
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
|
||||
'MAX(session_history.started) as last_watch ' \
|
||||
'FROM session_history ' \
|
||||
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
|
||||
@@ -625,7 +395,8 @@ class DataFactory(object):
|
||||
for item in result:
|
||||
row = {'platform': item[0],
|
||||
'total_plays': item[1],
|
||||
'last_play': item[2],
|
||||
'total_duration': item[2],
|
||||
'last_play': item[3],
|
||||
'platform_type': item[0],
|
||||
'title': '',
|
||||
'thumb': '',
|
||||
@@ -639,6 +410,7 @@ class DataFactory(object):
|
||||
top_platform.append(row)
|
||||
|
||||
home_stats.append({'stat_id': stat,
|
||||
'stat_type': sort_type,
|
||||
'rows': top_platform})
|
||||
|
||||
return home_stats
|
||||
@@ -658,7 +430,6 @@ class DataFactory(object):
|
||||
else:
|
||||
return None
|
||||
|
||||
print result
|
||||
stream_output = {}
|
||||
|
||||
for item in result:
|
||||
@@ -696,21 +467,21 @@ class DataFactory(object):
|
||||
try:
|
||||
if user_id:
|
||||
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
|
||||
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
|
||||
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE user_id = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
|
||||
result = monitor_db.select(query, args=[user_id, limit])
|
||||
elif user:
|
||||
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
|
||||
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
|
||||
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
|
||||
'FROM session_history_metadata ' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
|
||||
result = monitor_db.select(query, args=[user, limit])
|
||||
else:
|
||||
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
|
||||
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
|
||||
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
|
||||
'FROM session_history_metadata WHERE session_history.media_type != "track"' \
|
||||
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
|
||||
'ORDER BY started DESC LIMIT ?'
|
||||
@@ -720,111 +491,29 @@ class DataFactory(object):
|
||||
return None
|
||||
|
||||
for row in result:
|
||||
if row[1] == 'episode':
|
||||
thumb = row[5]
|
||||
if row[1] == 'episode' and row[6]:
|
||||
thumb = row[6]
|
||||
elif row[1] == 'episode':
|
||||
thumb = row[7]
|
||||
else:
|
||||
thumb = row[4]
|
||||
thumb = row[5]
|
||||
|
||||
recent_output = {'row_id': row[0],
|
||||
'type': row[1],
|
||||
'rating_key': row[2],
|
||||
'title': row[3],
|
||||
'parent_title': row[4],
|
||||
'thumb': thumb,
|
||||
'index': row[6],
|
||||
'parentIndex': row[7],
|
||||
'year': row[8],
|
||||
'time': row[9],
|
||||
'user': row[10]
|
||||
'index': row[8],
|
||||
'parent_index': row[9],
|
||||
'year': row[10],
|
||||
'time': row[11],
|
||||
'user': row[12]
|
||||
}
|
||||
recently_watched.append(recent_output)
|
||||
|
||||
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):
|
||||
monitor_db = database.MonitorDatabase()
|
||||
|
||||
@@ -841,13 +530,15 @@ class DataFactory(object):
|
||||
|
||||
metadata = {}
|
||||
for item in result:
|
||||
directors = item['directors'].split(';')
|
||||
writers = item['writers'].split(';')
|
||||
actors = item['actors'].split(';')
|
||||
genres = item['genres'].split(';')
|
||||
directors = item['directors'].split(';') if item['directors'] else []
|
||||
writers = item['writers'].split(';') if item['writers'] else []
|
||||
actors = item['actors'].split(';') if item['actors'] else []
|
||||
genres = item['genres'].split(';') if item['genres'] else []
|
||||
|
||||
metadata = {'type': item['media_type'],
|
||||
'rating_key': item['rating_key'],
|
||||
'parent_rating_key': item['parent_rating_key'],
|
||||
'grandparent_rating_key': item['grandparent_rating_key'],
|
||||
'grandparent_title': item['grandparent_title'],
|
||||
'parent_index': item['parent_media_index'],
|
||||
'parent_title': item['parent_title'],
|
||||
@@ -875,3 +566,47 @@ class DataFactory(object):
|
||||
}
|
||||
|
||||
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.'
|
||||
|
@@ -72,7 +72,7 @@ def check_active_sessions():
|
||||
monitor_db.action('UPDATE sessions SET paused_counter = ? '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[paused_counter, stream['session_key'], stream['rating_key']])
|
||||
if session['state'] == 'buffering':
|
||||
if session['state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0:
|
||||
# The stream is buffering so we need to increment the buffer_count
|
||||
# We're going just increment on every monitor ping,
|
||||
# would be difficult to keep track otherwise
|
||||
@@ -107,6 +107,12 @@ def check_active_sessions():
|
||||
plexpy.CONFIG.BUFFER_WAIT:
|
||||
logger.info(u"PlexPy Monitor :: User '%s' has triggered multiple buffer warnings."
|
||||
% stream['user'])
|
||||
# Set the buffer trigger time
|
||||
monitor_db.action('UPDATE sessions '
|
||||
'SET buffer_last_triggered = strftime("%s","now") '
|
||||
'WHERE session_key = ? AND rating_key = ?',
|
||||
[stream['session_key'], stream['rating_key']])
|
||||
|
||||
threading.Thread(target=notification_handler.notify,
|
||||
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
|
||||
|
||||
@@ -225,10 +231,10 @@ class MonitorProcessing(object):
|
||||
self.db.upsert('sessions', timestamp, keys)
|
||||
|
||||
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_details = data_factory.get_user_friendly_name(user=session['user'])
|
||||
user_data = users.Users()
|
||||
user_details = user_data.get_user_friendly_name(user=session['user'])
|
||||
|
||||
if session:
|
||||
logging_enabled = False
|
||||
|
@@ -1,4 +1,4 @@
|
||||
# This file is part of PlexPy.
|
||||
# This file is part of PlexPy.
|
||||
#
|
||||
# PlexPy is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
@@ -20,12 +20,12 @@ import time
|
||||
|
||||
|
||||
def notify(stream_data=None, notify_action=None):
|
||||
from plexpy import datafactory
|
||||
from plexpy import users
|
||||
|
||||
if stream_data and notify_action:
|
||||
# Check if notifications enabled for user
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_details = data_factory.get_user_friendly_name(user=stream_data['user'])
|
||||
user_data = users.Users()
|
||||
user_details = user_data.get_user_friendly_name(user=stream_data['user'])
|
||||
|
||||
if not user_details['do_notify']:
|
||||
return
|
||||
@@ -300,6 +300,7 @@ def build_notify_text(session, state):
|
||||
|
||||
duration = helpers.convert_milliseconds_to_minutes(item_metadata['duration'])
|
||||
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
|
||||
stream_duration = 0 if state == 'play' else int((time.time() - helpers.cast_to_float(session['started']) - helpers.cast_to_float(session['paused_counter'])) / 60)
|
||||
|
||||
progress_percent = helpers.get_percent(view_offset, duration)
|
||||
|
||||
@@ -317,10 +318,13 @@ def build_notify_text(session, state):
|
||||
'content_rating': item_metadata['content_rating'],
|
||||
'summary': item_metadata['summary'],
|
||||
'season_num': item_metadata['parent_index'],
|
||||
'season_num00': item_metadata['parent_index'].zfill(2),
|
||||
'episode_num': item_metadata['index'],
|
||||
'episode_num00': item_metadata['index'].zfill(2),
|
||||
'album_name': item_metadata['parent_title'],
|
||||
'rating': item_metadata['rating'],
|
||||
'duration': duration,
|
||||
'stream_duration': stream_duration,
|
||||
'progress': view_offset,
|
||||
'progress_percent': progress_percent
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@
|
||||
# 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, helpers, datafactory, http_handler, database
|
||||
from plexpy import logger, helpers, users, http_handler, database
|
||||
|
||||
from xml.dom import minidom
|
||||
|
||||
@@ -37,6 +37,18 @@ def refresh_users():
|
||||
"is_restricted": item['is_restricted']
|
||||
}
|
||||
|
||||
# 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 '
|
||||
'FROM users WHERE user_id = ?',
|
||||
[item['user_id']])
|
||||
if avatar_urls:
|
||||
if not avatar_urls[0]['custom_avatar_url'] or \
|
||||
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['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)
|
||||
|
||||
logger.info("Users list refreshed.")
|
||||
@@ -244,7 +256,7 @@ class PlexTV(object):
|
||||
|
||||
def get_synced_items(self, machine_id=None, user_id=None):
|
||||
sync_list = self.get_plextv_sync_lists(machine_id)
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_data = users.Users()
|
||||
|
||||
synced_items = []
|
||||
|
||||
@@ -268,8 +280,8 @@ class PlexTV(object):
|
||||
for device in sync_device:
|
||||
device_user_id = helpers.get_xml_attr(device, 'userID')
|
||||
try:
|
||||
device_username = data_factory.get_user_details(user_id=device_user_id)['username']
|
||||
device_friendly_name = data_factory.get_user_details(user_id=device_user_id)['friendly_name']
|
||||
device_username = user_data.get_user_details(user_id=device_user_id)['username']
|
||||
device_friendly_name = user_data.get_user_details(user_id=device_user_id)['friendly_name']
|
||||
except:
|
||||
device_username = ''
|
||||
device_friendly_name = ''
|
||||
|
@@ -15,7 +15,7 @@
|
||||
|
||||
import sqlite3
|
||||
|
||||
from plexpy import logger, helpers, monitor, datafactory, plextv
|
||||
from plexpy import logger, helpers, monitor, users, plextv
|
||||
from xml.dom import minidom
|
||||
|
||||
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)
|
||||
|
||||
monitor_processing = monitor.MonitorProcessing()
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_data = users.Users()
|
||||
|
||||
# Get the latest friends list so we can pull user id's
|
||||
try:
|
||||
@@ -292,8 +292,8 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
|
||||
continue
|
||||
|
||||
# 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']):
|
||||
user_id = data_factory.get_user_id(user=row['user'])
|
||||
if user_data.get_user_id(user=row['user']):
|
||||
user_id = user_data.get_user_id(user=row['user'])
|
||||
else:
|
||||
user_id = extracted_xml['user_id']
|
||||
|
||||
|
@@ -13,7 +13,7 @@
|
||||
# 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, helpers, datafactory, http_handler
|
||||
from plexpy import logger, helpers, users, http_handler
|
||||
from urlparse import urlparse
|
||||
|
||||
import plexpy
|
||||
@@ -218,6 +218,7 @@ class PmsConnect(object):
|
||||
recent_items = {'type': recent_type,
|
||||
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
|
||||
'title': helpers.get_xml_attr(item, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
||||
'thumb': helpers.get_xml_attr(item, 'thumb'),
|
||||
'added_at': helpers.get_xml_attr(item, 'addedAt')
|
||||
}
|
||||
@@ -232,6 +233,7 @@ class PmsConnect(object):
|
||||
recent_items = {'type': recent_type,
|
||||
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
|
||||
'title': helpers.get_xml_attr(item, 'title'),
|
||||
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
|
||||
'year': helpers.get_xml_attr(item, 'year'),
|
||||
'thumb': helpers.get_xml_attr(item, 'thumb'),
|
||||
'added_at': helpers.get_xml_attr(item, 'addedAt')
|
||||
@@ -505,7 +507,7 @@ class PmsConnect(object):
|
||||
"""
|
||||
def get_session_each(self, stream_type='', session=None):
|
||||
session_output = None
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_data = users.Users()
|
||||
if stream_type == 'track':
|
||||
|
||||
media_info = session.getElementsByTagName('Media')[0]
|
||||
@@ -531,7 +533,7 @@ class PmsConnect(object):
|
||||
transcode_container = ''
|
||||
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'))
|
||||
|
||||
if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Track'):
|
||||
@@ -639,7 +641,7 @@ class PmsConnect(object):
|
||||
else:
|
||||
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'))
|
||||
|
||||
if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Video'):
|
||||
|
523
plexpy/users.py
Normal file
523
plexpy/users.py
Normal 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
|
@@ -1 +1,2 @@
|
||||
PLEXPY_VERSION = "master"
|
||||
PLEXPY_RELEASE_VERSION = "1.1.3"
|
||||
|
@@ -241,3 +241,37 @@ def update():
|
||||
e
|
||||
)
|
||||
return
|
||||
|
||||
def read_changelog():
|
||||
|
||||
changelog_file = os.path.join(plexpy.PROG_DIR, 'CHANGELOG.md')
|
||||
|
||||
try:
|
||||
logfile = open(changelog_file, "r")
|
||||
except IOError, e:
|
||||
logger.error('PlexPy Version Checker :: Unable to open changelog file. %s' % e)
|
||||
return None
|
||||
|
||||
if logfile:
|
||||
output = ''
|
||||
lines = logfile.readlines()
|
||||
previous_line = ''
|
||||
for line in lines:
|
||||
if line[:2] == '# ':
|
||||
output += '<h3>' + line[2:] + '</h3>'
|
||||
elif line[:3] == '## ':
|
||||
output += '<h4>' + line[3:] + '</h4>'
|
||||
elif line[:2] == '* ' and previous_line.strip() == '':
|
||||
output += '<ul><li>' + line[2:] + '</li>'
|
||||
elif line[:2] == '* ':
|
||||
output += '<li>' + line[2:] + '</li>'
|
||||
elif line.strip() == '' and previous_line[:2] == '* ':
|
||||
output += '</ul></br>'
|
||||
else:
|
||||
output += line + '</br>'
|
||||
|
||||
previous_line = line
|
||||
|
||||
return output
|
||||
else:
|
||||
return '<h4>No changelog data</h4>'
|
@@ -13,7 +13,7 @@
|
||||
# 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, 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 mako.lookup import TemplateLookup
|
||||
@@ -65,7 +65,8 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def home(self):
|
||||
config = {
|
||||
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH
|
||||
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
|
||||
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE
|
||||
}
|
||||
return serve_template(templatename="index.html", title="Home", config=config)
|
||||
|
||||
@@ -118,9 +119,9 @@ class WebInterface(object):
|
||||
return json.dumps(formats)
|
||||
|
||||
@cherrypy.expose
|
||||
def home_stats(self, time_range='30', **kwargs):
|
||||
def home_stats(self, time_range='30', stat_type='0', **kwargs):
|
||||
data_factory = datafactory.DataFactory()
|
||||
stats_data = data_factory.get_home_stats(time_range=time_range)
|
||||
stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type)
|
||||
|
||||
return serve_template(templatename="home_stats.html", title="Stats", data=stats_data)
|
||||
|
||||
@@ -142,16 +143,15 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def user(self, user=None, user_id=None):
|
||||
user_data = users.Users()
|
||||
if user_id:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_details = data_factory.get_user_details(user_id=user_id)
|
||||
user_details = user_data.get_user_details(user_id=user_id)
|
||||
except:
|
||||
logger.warn("Unable to retrieve friendly name for user_id %s " % user_id)
|
||||
elif user:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
user_details = data_factory.get_user_details(user=user)
|
||||
user_details = user_data.get_user_details(user=user)
|
||||
except:
|
||||
logger.warn("Unable to retrieve friendly name for user %s " % user)
|
||||
else:
|
||||
@@ -162,13 +162,12 @@ class WebInterface(object):
|
||||
|
||||
@cherrypy.expose
|
||||
def edit_user_dialog(self, user=None, user_id=None, **kwargs):
|
||||
user_data = users.Users()
|
||||
if user_id:
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_friendly_name(user_id=user_id)
|
||||
result = user_data.get_user_friendly_name(user_id=user_id)
|
||||
status_message = ''
|
||||
elif user:
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_friendly_name(user=user)
|
||||
result = user_data.get_user_friendly_name(user=user)
|
||||
status_message = ''
|
||||
else:
|
||||
result = None
|
||||
@@ -186,15 +185,20 @@ class WebInterface(object):
|
||||
keep_history = kwargs.get('keep_history')
|
||||
else:
|
||||
keep_history = 0
|
||||
if 'thumb' in kwargs:
|
||||
custom_avatar = kwargs['thumb']
|
||||
else:
|
||||
custom_avatar = ''
|
||||
|
||||
user_data = users.Users()
|
||||
if user_id:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
data_factory.set_user_friendly_name(user_id=user_id,
|
||||
user_data.set_user_friendly_name(user_id=user_id,
|
||||
friendly_name=friendly_name,
|
||||
do_notify=do_notify,
|
||||
keep_history=keep_history)
|
||||
data_factory.set_user_profile_url(user_id=user_id,
|
||||
profile_url=kwargs['thumb'])
|
||||
user_data.set_user_profile_url(user_id=user_id,
|
||||
profile_url=custom_avatar)
|
||||
|
||||
status_message = "Successfully updated user."
|
||||
return status_message
|
||||
@@ -203,13 +207,12 @@ class WebInterface(object):
|
||||
return status_message
|
||||
if user:
|
||||
try:
|
||||
data_factory = datafactory.DataFactory()
|
||||
data_factory.set_user_friendly_name(user=user,
|
||||
user_data.set_user_friendly_name(user=user,
|
||||
friendly_name=friendly_name,
|
||||
do_notify=do_notify,
|
||||
keep_history=keep_history)
|
||||
data_factory.set_user_profile_url(user=user,
|
||||
profile_url=kwargs['thumb'])
|
||||
user_data.set_user_profile_url(user=user,
|
||||
profile_url=custom_avatar)
|
||||
|
||||
status_message = "Successfully updated user."
|
||||
return status_message
|
||||
@@ -239,11 +242,11 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def get_user_list(self, **kwargs):
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
users = data_factory.get_user_list(kwargs=kwargs)
|
||||
user_data = users.Users()
|
||||
user_list = user_data.get_user_list(kwargs=kwargs)
|
||||
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return json.dumps(users)
|
||||
return json.dumps(user_list)
|
||||
|
||||
@cherrypy.expose
|
||||
def checkGithub(self):
|
||||
@@ -449,6 +452,7 @@ class WebInterface(object):
|
||||
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
|
||||
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
|
||||
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
|
||||
"home_stats_type": checked(plexpy.CONFIG.HOME_STATS_TYPE),
|
||||
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
|
||||
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT
|
||||
}
|
||||
@@ -471,7 +475,7 @@ class WebInterface(object):
|
||||
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start",
|
||||
"tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop",
|
||||
"tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup",
|
||||
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote"
|
||||
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type"
|
||||
]
|
||||
for checked_config in checked_configs:
|
||||
if checked_config not in kwargs:
|
||||
@@ -758,8 +762,8 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs):
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_watch_time_stats(user_id=user_id, user=user)
|
||||
user_data = users.Users()
|
||||
result = user_data.get_user_watch_time_stats(user_id=user_id, user=user)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
|
||||
@@ -770,8 +774,8 @@ class WebInterface(object):
|
||||
@cherrypy.expose
|
||||
def get_user_platform_stats(self, user=None, user_id=None, **kwargs):
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
result = data_factory.get_user_platform_stats(user_id=user_id, user=user)
|
||||
user_data = users.Users()
|
||||
result = user_data.get_user_platform_stats(user_id=user_id, user=user)
|
||||
|
||||
if result:
|
||||
return serve_template(templatename="user_platform_stats.html", data=result,
|
||||
@@ -849,8 +853,8 @@ class WebInterface(object):
|
||||
elif user:
|
||||
custom_where = [['user', user]]
|
||||
|
||||
data_factory = datafactory.DataFactory()
|
||||
history = data_factory.get_user_unique_ips(kwargs=kwargs,
|
||||
user_data = users.Users()
|
||||
history = user_data.get_user_unique_ips(kwargs=kwargs,
|
||||
custom_where=custom_where)
|
||||
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
@@ -1254,3 +1258,32 @@ class WebInterface(object):
|
||||
|
||||
return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers",
|
||||
data=this_agent)
|
||||
|
||||
@cherrypy.expose
|
||||
def delete_history_rows(self, row_id, **kwargs):
|
||||
data_factory = datafactory.DataFactory()
|
||||
|
||||
if row_id:
|
||||
delete_row = data_factory.delete_session_history_rows(row_id=row_id)
|
||||
|
||||
if delete_row:
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return json.dumps({'message': delete_row})
|
||||
else:
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return json.dumps({'message': 'no data received'})
|
||||
|
||||
@cherrypy.expose
|
||||
def delete_all_user_history(self, user_id, **kwargs):
|
||||
data_factory = datafactory.DataFactory()
|
||||
|
||||
if user_id:
|
||||
delete_row = data_factory.delete_all_user_history(user_id=user_id)
|
||||
|
||||
if delete_row:
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return json.dumps({'message': delete_row})
|
||||
else:
|
||||
cherrypy.response.headers['Content-type'] = 'application/json'
|
||||
return json.dumps({'message': 'no data received'})
|
||||
|
||||
|
Reference in New Issue
Block a user