Compare commits

...

160 Commits

Author SHA1 Message Date
Tim
e386d3ee21 Merge branch 'dev' 2015-09-14 20:53:09 +02:00
Tim
285946bf94 v1.1.9
Close href tag on activity pane.
Update program description.
2015-09-14 20:45:49 +02:00
drzoidberg33
578ba52215 Merge pull request #185 from JonnyWong16/miscellaneous-fixes
Add tracks to user recently watched
2015-09-14 20:23:05 +02:00
Jonathan Wong
5126c39c26 Group recently watched tracks together by album
* Also group identical movie or episode
2015-09-14 00:02:31 -07:00
Jonathan Wong
5ec9e41244 Add tracks to user recently watched
* Clean up poster styles to match info pages
2015-09-13 23:28:42 -07:00
Tim
5eebf6592a Make music items clickable in activity pane now that we have info pages. 2015-09-14 01:09:12 +02:00
Tim
7994351644 Fix broken history logging. 2015-09-14 00:58:28 +02:00
drzoidberg33
e445228b8a Merge pull request #182 from JonnyWong16/music-info-pages
Music info pages
2015-09-13 23:42:54 +02:00
drzoidberg33
46e6250329 Merge pull request #183 from JonnyWong16/miscellaneous-fixes
Fix typo for location of PMS logs path
2015-09-13 23:28:23 +02:00
Jonathan Wong
2edfc1e3da Add link to music info from recently added 2015-09-13 14:09:22 -07:00
Jonathan Wong
f2024b0854 Fix typo for location of PMS logs path 2015-09-13 14:04:20 -07:00
Jonathan Wong
5f2cf6cb7a Add links for track items on tables 2015-09-13 13:33:09 -07:00
Jonathan Wong
73664b6a03 Update info page to show music info 2015-09-13 13:32:44 -07:00
Jonathan Wong
af131ce16d Change code to get generic library children items
* Merge the separate season list and episode list
2015-09-13 13:31:51 -07:00
Jonathan Wong
414c4c2ffa Add artist, album, and track metadata 2015-09-13 13:29:04 -07:00
drzoidberg33
d2cdc2cea2 Merge pull request #180 from JonnyWong16/miscellaneous-fixes
Add photos to current activity
2015-09-13 20:09:26 +02:00
Jonathan Wong
b860f9a5e8 Add photos to current activity
* Also fix BIF fade in
2015-09-12 23:09:01 -07:00
drzoidberg33
e5535b6167 Merge pull request #177 from JonnyWong16/miscellaneous-fixes
Fix paused time on graphs and other miscellaneous items
2015-09-13 01:44:46 +02:00
Jonathan Wong
6359c02c80 Fix PMS Token input error message on settings page 2015-09-11 21:17:11 -07:00
Jonathan Wong
1c589fbefa Change season and episode number in history table item title
* For seasons/episodes of any number of digits
2015-09-11 21:09:48 -07:00
Jonathan Wong
b768ad8a19 Fix Mystery platform names in graphs 2015-09-11 17:41:25 -07:00
Jonathan Wong
60878ed12e Add table name session_history to paused_counter in watch stats query 2015-09-11 15:40:24 -07:00
Jonathan Wong
328d744efd Swap order of History and Users in the navbar 2015-09-11 15:19:30 -07:00
Jonathan Wong
92a868c3c6 Clean up watch statistics duration database queries 2015-09-11 13:35:50 -07:00
Jonathan Wong
7b9210a5fc Add music to graphs
* Separate out TV, movies, and music on bar graphs
* Separate out direct play, direct stream, and transcode on bar graphs
2015-09-11 13:12:33 -07:00
Jonathan Wong
fb872596d6 Fix paused time from graphs 2015-09-11 11:11:32 -07:00
Jonathan Wong
a43efef28a Fix write session to history only if library item
Some channel items, specifically Apple movie trailers, are counted as
movie items. Make sure the rating_key is a number to filter these out.
2015-09-10 21:30:34 -07:00
Jonathan Wong
e30e6dfe35 Fix Plex Web click through logo 2015-09-10 15:03:38 -07:00
Tim
eaadd5e2d6 Merge branch 'dev' 2015-09-09 22:20:45 +02:00
Tim
c608c7c9fc v1.1.8 2015-09-09 22:19:59 +02:00
drzoidberg33
451485d706 Merge pull request #173 from JonnyWong16/miscellaneous-fixes
Change Plex click through to app.plex.tv instead of local IP
2015-09-09 22:16:05 +02:00
Jonathan Wong
849675185d Change Plex click through to app.plex.tv instead of local IP 2015-09-09 13:13:17 -07:00
drzoidberg33
b060a23733 Merge pull request #170 from JonnyWong16/miscellaneous-fixes
Miscellaneous
2015-09-09 21:51:11 +02:00
drzoidberg33
bee9091182 Add Gitter Button to README 2015-09-09 16:49:28 +02:00
Jonathan Wong
f8e1ba6798 Add link to poster on info pages to view item in Plex/Web 2015-09-09 02:13:52 -07:00
Tim
e5ce57fead Fix bug with missing tagline field in PlexWatch db import. 2015-09-08 21:30:09 +02:00
Jonathan Wong
c9e2d1d200 Fix delete mode on info pages 2015-09-08 10:35:33 -07:00
Jonathan Wong
88b6eae3bf Add platform icons for windows and windows phone 2015-09-08 09:09:16 -07:00
drzoidberg33
c7d6ee8021 Merge pull request #166 from JonnyWong16/miscellaneous-fixes
Fix home stats title overflow
2015-09-08 00:44:26 +02:00
Jonathan Wong
7480508af2 Fix home stats title overflow 2015-09-07 15:38:34 -07:00
Tim
3fcc44aacf Merge branch 'dev' 2015-09-07 21:16:30 +02:00
Tim
fcc4575a86 v1.1.7 2015-09-07 21:15:52 +02:00
Tim
ebd0276eae Fix bug when parsing custom notification strings with unicode characters. 2015-09-07 20:43:15 +02:00
drzoidberg33
35521e127f Merge pull request #163 from JonnyWong16/miscellaneous-fixes
Fix for 5 digit library stats
2015-09-07 19:22:04 +02:00
Jonathan Wong
2c83554631 Update documentation 2015-09-06 21:13:56 -07:00
Jonathan Wong
56b717b1a1 Add transcode progress and speed to dashboard activity 2015-09-06 21:10:47 -07:00
Jonathan Wong
56f601e2a5 Add icon for play/pause/buffer state in dashboard activity 2015-09-06 15:52:47 -07:00
Jonathan Wong
3867dd7bdd Add throttled indicator to dashboard activity 2015-09-06 15:51:46 -07:00
Jonathan Wong
b7dc28c3fb Remove unnecessary console.log 2015-09-06 13:33:35 -07:00
Jonathan Wong
e6383d52ad Fix adding tagline to session_history_metadata table 2015-09-06 13:21:08 -07:00
Jonathan Wong
0a2ebb8815 Add tagline for newly watched items 2015-09-06 12:57:51 -07:00
Jonathan Wong
e44a0fed22 Fix tagline only if type is movie 2015-09-06 12:46:46 -07:00
Jonathan Wong
2d7585d64b Add tagline to movie info metadata 2015-09-06 12:31:50 -07:00
Jonathan Wong
df290d995b Make video preview thumbnails wording clearer. 2015-09-06 12:17:55 -07:00
Jonathan Wong
36ee5234b1 Revert overflow: hidden
* Accidentally hid top stats slider.
2015-09-06 11:54:31 -07:00
Jonathan Wong
48e601d5ff Restore edit username in users table
* Editing usernames was accidentally removed in e7b1e17
2015-09-06 11:20:49 -07:00
Jonathan Wong
84aa727387 Fix for 5 digit library stats 2015-09-06 11:03:47 -07:00
Tim
29204cb6ba Fix weird issue with bad merge. 2015-09-06 15:33:17 +02:00
Tim
6b75a55ce8 Merge branch 'dev'
# Conflicts:
#	data/interfaces/default/info.html
2015-09-06 15:23:38 +02:00
Tim
527e7be13d v1.1.6 2015-09-06 14:41:40 +02:00
Tim
2d739f64cf Fix some issues where some channel streams are being reported as episodes.
Fix images for activity pane when channels/clips are being played.
2015-09-06 13:49:09 +02:00
drzoidberg33
39ae387e0e Merge pull request #160 from JonnyWong16/miscellaneous-fixes
Fix mobile layout for home and info pages
2015-09-06 11:58:27 +02:00
Jonathan Wong
aaa1f0aa33 Change mobile layout for devices up to 400px wide 2015-09-05 18:08:21 -07:00
Jonathan Wong
423360e820 Simplify css for homepage mobile layout fix 2015-09-05 18:01:44 -07:00
Jonathan Wong
52fae6df0e Fix dashboard activity and home stats for mobile layout
* Specify dashboard activity as 100% width instead of a set px width
2015-09-05 17:41:48 -07:00
Jonathan Wong
58ea4a65e1 Fix info pages for mobile layout 2015-09-05 17:41:46 -07:00
drzoidberg33
82025457ba Merge pull request #157 from JonnyWong16/miscellaneous-fixes
Fix top platforms sort by duration
2015-09-05 10:16:28 +02:00
Jonathan Wong
9d60ed7174 Fix top platforms sort by duration 2015-09-04 23:03:16 -07:00
drzoidberg33
d67f903107 Merge pull request #146 from JonnyWong16/server-stats
Rename server statistics to library statistics
2015-08-30 22:02:04 +02:00
Jonathan Wong
389ce97d03 Rename server statistics to library statistics 2015-08-30 11:01:52 -07:00
Tim
98f5c33af7 Fix album art on recently added. 2015-08-30 17:54:45 +02:00
drzoidberg33
dceab3791f Merge pull request #145 from JonnyWong16/server-stats
Initial implementation of server statistics
2015-08-30 17:46:04 +02:00
Jonathan Wong
d44bd2f35b Shift secondary count right to account for 4 digit items 2015-08-29 13:24:44 -07:00
Jonathan Wong
82fc314b35 Another 1px off fix 2015-08-29 13:20:21 -07:00
Jonathan Wong
6ff902e653 Fix playcounts 1px off 2015-08-29 13:16:03 -07:00
Jonathan Wong
0245668907 Add "items" text 2015-08-29 13:15:29 -07:00
Jonathan Wong
3436175223 Initial implementation of server statistics 2015-08-29 12:15:11 -07:00
Tim
8138c27242 Fix reversed column names on user history table. 2015-08-29 00:59:51 +02:00
Tim
5705e677d4 fixed conflicts 2015-08-29 00:32:52 +02:00
Tim
c2417de895 Add "Local" as the default unauthenticated user to the local database. 2015-08-29 00:18:09 +02:00
Jonathan Wong
9b03ffbaa1 Fix season_list 2015-08-28 13:05:45 -07:00
Jonathan Wong
53c98d0acc Fix typo 2015-08-28 11:32:58 -07:00
Tim
883a183208 Fix broken bif indexes on activity pane.
Fix incorrect video dimensions when media transcoded on activity pane.
2015-08-28 11:40:22 +02:00
Jonathan Wong
85a3f15531 Fix platform_type missing for last watched home stats 2015-08-28 01:31:35 -07:00
Jonathan Wong
28d136075c Add Last Watched to home stats 2015-08-27 18:30:43 -07:00
Jonathan Wong
a682cd31af Remove video fields from stream info modal if item is a track 2015-08-27 15:38:39 -07:00
Jonathan Wong
9fc44f793b Change episodes default to art on dashboard 2015-08-27 15:31:54 -07:00
Jonathan Wong
16b1b2a781 Add episode number to info navbar 2015-08-27 14:41:12 -07:00
drzoidberg33
6e14c08570 Merge pull request #142 from JonnyWong16/top-lists
Dashboard activity
2015-08-27 23:32:52 +02:00
drzoidberg33
b5f1475a90 Merge pull request #141 from JonnyWong16/tables
Tables
2015-08-27 23:06:16 +02:00
Jonathan Wong
8efb37c4ec Fix merging conflict 2015-08-27 14:02:16 -07:00
Jonathan Wong
ab78b59f61 Update graphs history table modal
* Tooltips
* Stream details modal
2015-08-27 13:57:53 -07:00
Jonathan Wong
e7b1e177d2 Fix group purge on users table to remember state on table redraw
* Changed popover to alert box
* Changed button colour
2015-08-27 13:57:50 -07:00
Jonathan Wong
e41fa7a08e Make sure buttons are cleared when entering delete mode 2015-08-27 13:57:48 -07:00
Jonathan Wong
0f10c21a66 Change alert and button color on info and user history tables 2015-08-27 13:57:46 -07:00
Tim
f2b9984cd6 fixed conflicts 2015-08-27 22:34:09 +02:00
Jonathan Wong
99dd133a9a Increase home stats card width to match dashboard activity 2015-08-27 13:00:40 -07:00
Jonathan Wong
7bbcc575b0 Style current activity to match Plex/Web 2015-08-27 12:50:31 -07:00
Tim
0fded38bfb fix conflicts 2015-08-27 21:49:53 +02:00
drzoidberg33
88a651645a Merge pull request #139 from JonnyWong16/miscellaneous-fixes
Create body container so scrollbar is beneath navbar
2015-08-27 21:11:13 +02:00
Jonathan Wong
60380c6a99 Add season list to show page, and watch history to season page 2015-08-27 00:48:10 -07:00
Jonathan Wong
298d98cee9 Fix settings page error alerts 2015-08-27 00:05:43 -07:00
Jonathan Wong
b91a4844e0 Cleanup dashboard activity styles 2015-08-26 23:45:19 -07:00
Jonathan Wong
f00de8e548 Another clipping text fix 2015-08-26 20:41:05 -07:00
Jonathan Wong
dfbf435c77 Fix clipping on long titles 2015-08-26 20:34:06 -07:00
Jonathan Wong
dd7390d111 Fix box shadow on slider 2015-08-26 20:17:42 -07:00
Jonathan Wong
73125153c8 Update graphs history table modal
* Tooltips
* Stream details modal
2015-08-26 19:36:08 -07:00
Jonathan Wong
c08d581df3 Fix group purge on users table to remember state on table redraw
* Changed popover to alert box
* Changed button colour
2015-08-26 18:20:29 -07:00
Jonathan Wong
ef5d9d6604 Make sure buttons are cleared when entering delete mode 2015-08-26 18:18:40 -07:00
Jonathan Wong
84ffcc7948 Change alert and button color on info and user history tables 2015-08-26 17:56:17 -07:00
Jonathan Wong
53830a7711 Fix info navbar overflow 2015-08-26 17:29:05 -07:00
Jonathan Wong
a126703f44 Fix content-wrapper background 2015-08-26 17:29:02 -07:00
Jonathan Wong
6720d696eb Fix typo 2015-08-26 17:29:00 -07:00
Jonathan Wong
7328fcd0fb Create body container so scrollbar is beneath navbar 2015-08-26 17:28:55 -07:00
Jonathan Wong
1e57e952db Overhaul of info pages
* Updated style to match Plex/Web
* Full page background art
* Larger posters
* Navigation bar
2015-08-26 17:01:17 -07:00
Jonathan Wong
d627e0efdb Create body container so scrollbar is beneath navbar 2015-08-26 16:23:54 -07:00
Jonathan Wong
2193d06363 Only toggle top lists on click
Fixed borders on the slider
2015-08-26 16:04:06 -07:00
Jonathan Wong
3fc573bd90 Remove console.log and more css cleanup 2015-08-26 16:04:04 -07:00
Jonathan Wong
9b9088bd5e Fixed alert messages on settings validation 2015-08-26 16:04:02 -07:00
Jonathan Wong
53fb8b999f Added chevrons and more reliable animations 2015-08-26 16:04:00 -07:00
Jonathan Wong
58efd299cc Add setting to change the number of items in the top lists 2015-08-26 16:03:58 -07:00
Jonathan Wong
8ae2f718f4 Initial implementation of homepage top lists
* Top lists for all stats, default max 5 items
2015-08-26 16:03:56 -07:00
Tim
ff0ed1abe4 Fix info pages bugging out if non-existent rating key is passed. 2015-08-27 00:35:29 +02:00
Tim
41e1fee4b4 Match user refreshes on user_id not username as it causes issues if usernames change. 2015-08-26 23:53:42 +02:00
Tim
b308e92c9d Merge branch 'dev' 2015-08-26 12:26:43 +02:00
Tim
82808cdc97 v1.1.5 2015-08-26 12:26:02 +02:00
Tim
6d5e7135ae v1.1.4 2015-08-26 12:08:50 +02:00
Tim
4a75e372d9 v1.1.4
Version number increment.
2015-08-26 11:54:19 +02:00
Tim
a37ca3f29c v1.1.4
Hide show/hide option on users table until it does something.
2015-08-26 11:52:51 +02:00
Tim
9016fc41f9 Add user submitted systemd scripts. 2015-08-26 11:28:07 +02:00
Tim
f56ba300ac Don't refresh users on every config save.
Don't reschedule all tasks on every config save.
Send pre-formatted month+year string for monthly total plays graph.
2015-08-26 00:11:20 +02:00
drzoidberg33
0a850fc3af Merge pull request #134 from JonnyWong16/history-tables
Change alert message and button colour
2015-08-25 23:52:57 +02:00
Jonathan Wong
5124ebfe16 Change alert message and button colour 2015-08-25 14:43:08 -07:00
drzoidberg33
4de252d8e4 Merge pull request #126 from JonnyWong16/history-tables
History tables
2015-08-25 22:29:26 +02:00
Jonathan Wong
c5237547cf Fixed thumbnail for high DPI screens 2015-08-25 12:31:36 -07:00
Jonathan Wong
af85d36762 Also remember state on user history and info pages 2015-08-24 14:38:54 -07:00
Jonathan Wong
41c94741e2 Remember selected delete state on table redraw 2015-08-24 14:21:21 -07:00
drzoidberg33
8b07b53b43 Merge pull request #127 from JonnyWong16/miscellaneous-fixes
Miscellaneous fixes
2015-08-24 18:34:35 +02:00
Jonathan Wong
445dd1250f Revert "Fix season info page episode picture dimensions" 2015-08-24 04:04:30 -07:00
Jonathan Wong
4df8014102 Revert "Fix size of posters pulled from Plex"
This reverts commit 81a8d93336.

Conflicts:
	data/interfaces/default/home_stats.html
2015-08-24 03:57:18 -07:00
drzoidberg33
35320649db Merge pull request #124 from JonnyWong16/edit-users
Add modal confirm box for purging of individual user history
2015-08-24 12:32:02 +02:00
Jonathan Wong
c62ec71cfc Simplify css for hover borders 2015-08-23 23:41:57 -07:00
Jonathan Wong
b685d17969 Make user profile picture a link on users table 2015-08-23 23:41:35 -07:00
Jonathan Wong
ba5fd4b9d3 Fix season info page episode picture dimensions 2015-08-23 23:26:28 -07:00
Jonathan Wong
79757da2b1 Update user images 2015-08-23 23:25:55 -07:00
Jonathan Wong
77f5224e13 Moved settings around to more logical locations 2015-08-23 23:23:23 -07:00
Jonathan Wong
31805e39e6 Change class name for consistency 2015-08-23 17:46:44 -07:00
Jonathan Wong
ff6d43c398 Fixed numbers swapped 2015-08-23 17:06:05 -07:00
Jonathan Wong
81a8d93336 Fix size of posters pulled from Plex 2015-08-23 15:58:16 -07:00
Jonathan Wong
609549f974 Also add group delete to user history and info pages 2015-08-23 15:33:41 -07:00
Jonathan Wong
7ba6d704cd Add group delete feature to history table
* This helps to prevent accidental row deletes
2015-08-23 15:27:36 -07:00
Jonathan Wong
0ff1b4d5dd Add modal confirm box for purging of individual user history 2015-08-23 14:37:42 -07:00
Tim
7eac521369 Remove max width restriction on home stats cards. 2015-08-23 23:01:10 +02:00
drzoidberg33
1d8a16f3c4 Merge pull request #120 from JonnyWong16/edit-users
Fix popover relocating on window resizing
2015-08-23 22:35:20 +02:00
Jonathan Wong
685f8cabdb Fix popover relocating on window resizing
Also stay under the navbar if scrolling down the page.
2015-08-23 11:20:36 -07:00
drzoidberg33
70141eda22 Merge pull request #118 from JonnyWong16/edit-users
Add group editing of users from the user table
2015-08-23 15:52:50 +02:00
Jonathan Wong
0204478483 Add group editing of users from the user table
From the user table, you can now:
* Change the friendly name
* Toggle notifications
* Toggle keep history
* Purge all user history (as a group selection)
2015-08-23 04:32:42 -07:00
drzoidberg33
79f317709b Merge pull request #111 from JonnyWong16/info-pages
Info pages
2015-08-23 00:18:25 +02:00
Tim
ac3bafcd02 Fix stream_duration when notify state is play. 2015-08-22 23:57:17 +02:00
Tim
ac9a8b85ec Fix stream_duration if no paused_duration and format correctly. 2015-08-22 22:11:47 +02:00
53 changed files with 4467 additions and 1538 deletions

View File

@@ -1,21 +1,88 @@
# Changelog
## v1.1.9 (2015-09-14)
* Another JonnyWong release. I'm going to stop thanking you now ;)
* Add music plays to graphs.
* Add info pages for music items.
* Add music to user recently watched items.
* Add photo views to Activity pane (photos are not logged).
* Fix token validation message on Settings page.
* Fix some "Mystery" platform names.
* Fix paused time be counted for graph data.
* Other small bug fixes.
## v1.1.8 (2015-09-09)
* Add platform images for Windows devices. Thanks @JonnyWong.
* Add click-through to PlexWeb preplay page from info page. Thanks @JonnyWong.
* Fix broken delete option on info pages. Thanks @JonnyWong.
* Fix tagline bug in PlexWatch db import tool.
* Fix home stats text overflow bug. Thanks @JonnyWong.
## v1.1.7 (2015-09-07)
* Show tagline in info screens for movies. Thanks @JonnyWong.
* Add play/pause/buffer icon to activity pane. Thanks @JonnyWong.
* Add transcoder info in activity pane info. Thanks @JonnyWong.
* Show transcoder progress on activity progress bar. Thanks @JonnyWong.
* Fix bug where custom notification strings would be ignored if unicode characters were present.
* Fix text overflow issue on home stats cards. Thanks @JonnyWong.
* Fix regression with user friendly name change input in edit screen. Thanks @JonnyWong.
## v1.1.6 (2015-09-06)
* Home stats cards are now expandable to show multiple items. Configurable in settings. Thanks @JonnyWong.
* Completely redesigned media info pages. Thanks @JonnyWong.
* Redesigned activity pane to match Plex Web more closely. Thanks @JonnyWong.
* New Library stats on home page, shows total item counts per library. Thanks @JonnyWong.
* New last watched card in home stats. Shows last watched items. Thanks @JonnyWong.
* Improved some layout issues on mobile devices. Thanks @JonnyWong.
* Fixed issue where some clip/channel items are reported as episodes and causing exceptions.
* Many styling improvements and fixes. Thanks @JonnyWong.
* Fixed incorrect sort on home stats platform count by duration. Thanks @JonnyWong.
* Fix issue where user refresh would continually be called as "Local" user didn't exist in database.
* Fixed styling on graph stream modal. Thanks @JonnyWong.
* Fixed some issues with users page editing. Thanks @JonnyWong.
* Fix error page when clicking through to an item that no longer exists.
## v1.1.5 (2015-08-27)
* Fix git tag being one release behind.
## v1.1.4 (2015-08-26)
* User info is now editable from the users table. Thanks @JonnyWong.
* Improved delete mode for history pages - able to multi-select now. Thanks @JonnyWong.
* Improved image quality on tooltip images.
* More styling improvements and fixes on user and info pages. Thanks @JonnyWong.
* Added some user submitted systemd init scripts. Thanks @malle-pietje and @artbird309.
* Fixed some background operations when saving settings.
* Fix max width restricting home stats to 1600px.
* Fix stream duration parameter for notifications when paused counter is null.
## v1.1.3 (2015-08-22)
* 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.
* Two digit season and episode numbers for custom notification messages. Thanks @JonnyWong.
* New FreeNAS init script. Thanks @JonnyWong.
* Lots of styling improvements! Thanks @JonnyWong.
* Graph page remembers last selected options. Thanks @JonnyWong.
* New Popular movie homepage stats. Thanks @JonnyWong.
* Add option for duration vs play count on home stats. (Settings -> Extra Settings). Thanks @JonnyWong.
* Clean up media info pages. Don't show metadata that is missing. Thanks @JonnyWong.
* Add clear button to search inputs. Thanks @JonnyWong.
* New columns on Users list. Thanks @JonnyWong.
* New stream duration option for custom notification messages. Thanks @JonnyWong.
* Rad new tooltips on the history pages. Thanks @JonnyWong.
* And a lot of small visual changes and fixes. Thanks @JonnyWong.
* Fixed IP address modal on user history page.
* Fixed "invalid date" showing on monthly plays graph.

View File

@@ -64,7 +64,7 @@ def main():
# Set up and gather command line arguments
parser = argparse.ArgumentParser(
description='Python frontend for PlexWatch.')
description='A Python based monitoring and tracking tool for Plex Media Server.')
parser.add_argument(
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')

View File

@@ -1,5 +1,7 @@
#PlexPy
[![Join the chat at https://gitter.im/drzoidberg33/plexpy](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/drzoidberg33/plexpy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
A python based web application for monitoring, analytics and notifications for Plex Media Server (www.plex.tv).
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).

View File

@@ -1,4 +1,4 @@
<%
<%
import plexpy
from plexpy import version
%>
@@ -52,7 +52,7 @@ from plexpy import version
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="home">
<img alt="PlexPy" src="interfaces/default/images/logo-plexpy@2x.png" height="40px">
<img alt="PlexPy" src="interfaces/default/images/logo-plexpy@2x.png" height="40">
</a>
</div>
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
@@ -62,16 +62,16 @@ from plexpy import version
% else:
<li><a href="home"><i class="fa fa-lg fa-home"></i></a></li>
% endif
% if title=="History":
<li class="active"><a href="history">History</a></li>
% else:
<li><a href="history">History</a></li>
% endif
% if title=="Users" or title=="User":
<li class="active"><a href="users">Users</a></li>
% else:
<li><a href="users">Users</a></li>
% endif
% if title=="History":
<li class="active"><a href="history">History</a></li>
% else:
<li><a href="history">History</a></li>
% endif
% if title=="Graphs":
<li class="active"><a href="graphs">Graphs</a></li>
% else:
@@ -99,7 +99,9 @@ from plexpy import version
</div>
${next.headerIncludes()}
<div class="body-container">
${next.body()}
</div>
<script src="interfaces/default/js/jquery-2.1.4.min.js"></script>
<script src="interfaces/default/js/bootstrap3/bootstrap.min.js"></script>

File diff suppressed because it is too large Load Diff

View File

@@ -28,10 +28,15 @@ user Returns the name of the user owning the session.
user_id Returns the Plex user id if available.
machine_id Returns the machine id of the players being used.
friendly_name Returns the friendlly name of the user owning the session.
user_thumb Returns the profile picture of the user owning the session.
state Returns the state of the current session. Either 'playing', 'paused' or 'buffering'.
title Returns the name of the episode, movie or music track.
year Returns the year of the episode, movie, or clip.
player Returns the name of the platform used to play the stream.
platform Returns the type of platform used to play the stream.
throttled Returns true if the transcode session is throttled.
transcode_progress Returns the current transcode progress of the item. 0 to 100.
transcode_speed Returns the current transcode speed of the item.
audio_decision Returns the audio transcode decision. Either 'transcode', 'copy' or 'direct play'.
audio_codec Returns the name of the audio codec.
audio_channels Returns the number of audio channels.
@@ -61,55 +66,147 @@ DOCUMENTATION :: END
% if data is not None:
% if data['stream_count'] != '0':
% for a in data['sessions']:
<div class="instance" id="instance-${a['session_key']}">
<div class="poster">
<div class="dashboard-activity-poster-face">
<div class="dashboard-instance" id="instance-${a['session_key']}">
% if a['type'] == 'movie' or a['type'] == 'episode' or a['type'] == 'track':
<a href="info?item_id=${a['rating_key']}">
% endif
<div class="dashboard-activity-poster">
% if a['type'] == 'movie' and not a['indexes']:
<img src="pms_image_proxy?img=${a['art']}&width=410&height=230"/>
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['art']}&width=500&height=280);"></div>
% elif a['type'] == 'episode' and not a['indexes']:
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['art']}&width=500&height=280);"></div>
% elif a['indexes']:
<img onload="fadeIn(this)" src="pms_image_proxy?img=${a['bif_thumb']}&width=300&height=169" style="display: none;"/>
<div class="dashboard-activity-poster-face bif" style="background-image: url(pms_image_proxy?img=${a['bif_thumb']}&width=500&height=280); display: none;"></div>
% else:
% if a['type'] == 'track':
<div class="dashboard-activity-poster-music-bg" style="background-image: url('pms_image_proxy?img=${a['thumb']}&width=300&height=300');"></div>
% endif
% if a['type'] == 'clip':
<img src="${a['thumb']}"/>
<div class="dashboard-activity-cover-face-bg" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=300);"></div>
<div class="dashboard-activity-cover-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=300);"></div>
% elif a['type'] == 'clip':
% if a['art'][:4] == 'http':
<div class="dashboard-activity-poster-face" style="background-image: url(${a['art']});"></div>
% elif a['thumb'][:4] == 'http':
<div class="dashboard-activity-poster-face" style="background-image: url(${a['thumb']});"></div>
% else:
<img src="pms_image_proxy?img=${a['thumb']}&width=410&height=230&fallback=cover"/>
% endif
% endif
<div class="dashboard-activity-poster-info-bar">
<div class="dashboard-activity-poster-info-text">
% if a['type'] == 'track':
Track ${a['media_index']}
% elif a['type'] == 'episode':
Season ${a['parent_media_index']}, Episode ${a['media_index']}
% if a['art']:
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['art']}&width=500&height=280);"></div>
% else:
${a['title']}
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=500&height=280);"></div>
% endif
% endif
% elif a['type'] == 'photo':
<div class="dashboard-activity-poster-face bif" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=500&height=500);"></div>
% else:
<div class="dashboard-activity-cover-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=300&fallback=cover);"></div>
% endif
% endif
<div class="dashboard-activity-button-info">
<button type="button" class="btn btn-activity-info btn-lg" data-target="#stream-${a['session_key']}">
<i class="fa fa-info-circle"></i>
</button>
</div>
<div id="stream-${a['session_key']}" class="dashboard-activity-info-details-overlay">
<div class="dashboard-activity-info-details-content">
<div class="dashboard-activity-info-platform" id="platform-${a['session_key']}">
<strong>${a['player']}</strong><br />
% if a['state'] == 'playing':
State &nbsp;<strong>Playing</strong>
% elif a['state'] == 'paused':
State &nbsp;<strong>Paused</strong>
% elif a['state'] == 'buffering':
State &nbsp;<strong>Buffering</strong>
% endif
</div>
% if a['type'] == 'track':
% if a['audio_decision'] == 'direct play':
Stream &nbsp;<strong>Direct Play</strong>
% else:
Stream &nbsp;<strong>Transcoding
(Speed: ${a['transcode_speed']})
% if a['throttled'] == '1':
(Throttled)
% endif
</strong>
% endif
<br />
% if a['audio_decision'] == 'direct play':
Audio &nbsp;<strong>Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch)</strong>
% elif a['audio_decision'] == 'Copy':
Audio &nbsp;<strong>Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
% elif a['audio_decision'] != 'transcode':
Audio &nbsp;<strong>Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
% endif
% elif a['type'] == 'episode' or a['type'] == 'movie' or a['type'] == 'clip':
% if a['video_decision'] == 'direct play':
Stream &nbsp;<strong>Direct Play</strong>
% else:
Stream &nbsp;<strong>Transcoding
(Speed: ${a['transcode_speed']})
% if a['throttled'] == '1':
(Throttled)
% endif
</strong>
% endif
<br />
% if a['video_decision'] == 'direct play':
Video &nbsp;<strong>Direct Play (${a['video_codec']}) (${a['width']}x${a['height']})</strong>
% elif a['video_decision'] == 'copy':
Video &nbsp;<strong>Direct Stream (${a['transcode_video_codec']}) (${a['width']}x${a['height']})</strong>
% elif a['video_decision'] == 'transcode':
Video &nbsp;<strong>Transcode (${a['transcode_video_codec']}) (${a['transcode_width']}x${a['transcode_height']})</strong>
% endif
<br />
% if a['audio_decision'] == 'direct play':
Audio &nbsp;<strong>Direct Play (${a['audio_codec']}) (${a['audio_channels']}ch)</strong>
% elif a['audio_decision'] == 'copy':
Audio &nbsp;<strong>Direct Stream (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
% elif a['audio_decision'] == 'transcode':
Audio &nbsp;<strong>Transcode (${a['transcode_audio_codec']}) (${a['transcode_audio_channels']}ch)</strong>
% endif
% elif a['type'] == 'photo':
% if a['video_decision'] == 'direct play':
Stream &nbsp;<strong>Direct Play</strong>
% else:
Stream &nbsp;<strong>
Transcoding
(Speed: ${a['transcode_speed']})
% if a['throttled'] == '1':
(Throttled)
% endif
</strong>
% endif
% endif
<br>
</div>
</div>
% if a['type'] != 'photo':
<div class="dashboard-activity-poster-info-bar">
<div class="dashboard-activity-poster-info-time">
<span class="progress_time">${a['progress']}</span>/<span class="progress_time">${a['duration']}</span>
</div>
</div>
% endif
</div>
<div class='dashboard-activity-metadata-wrapper'>
<div class='dashboard-activity-instance-overlay'>
<div class='dashboard-activity-metadata-progress-minutes'>
<div class='activity-progress'>
% if a['type'] == 'movie' or a['type'] == 'episode' or a['type'] == 'track':
</a>
% endif
<div class="dashboard-activity-progress">
<div class="dashboard-activity-progress-bar">
<div class="bufferbar" style="width: ${a['transcode_progress']}%">${a['transcode_progress']}%</div>
<div class="bar" style="width: ${a['progress_percent']}%">${a['progress_percent']}%</div>
</div>
</div>
<div class="dashboard-activity-metadata-platform" data-toggle="tooltip" data-placement="right" title data-title="${a['player']}" id="platform-${a['session_key']}">
</div>
<div class="dashboard-activity-metadata-user">
% if a['user_id']:
<a href="user?user_id=${a['user_id']}">${a['friendly_name']}</a> is ${a['state']}
% else:
<a href="user?user=${a['user']}">${a['friendly_name']}</a> is ${a['state']}
% endif
</div>
<div class="dashboard-activity-metadata-wrapper">
<a href="user?user_id=${a['user_id']}">
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${a['user_thumb']});"></div>
</a>
<div class="dashboard-activity-metadata-title">
% if a['state'] == 'playing':
<i class="fa fa-play"></i>&nbsp;
% elif a['state'] == 'paused':
<i class="fa fa-pause"></i>&nbsp;
% elif a['state'] == 'buffering':
<i class="fa fa-spinner"></i>&nbsp;
% endif
% if a['type'] == 'episode':
<a href="info?item_id=${a['rating_key']}">${a['grandparent_title']} - ${a['title']}</a>
% elif a['type'] == 'movie':
@@ -117,103 +214,55 @@ DOCUMENTATION :: END
% elif a['type'] == 'clip':
${a['title']}
% elif a['type'] == 'track':
${a['grandparent_title']} - ${a['title']}
<a href="info?item_id=${a['rating_key']}">${a['grandparent_title']} - ${a['title']}</a>
% elif a['type'] == 'photo':
${a['parent_title']}
% else:
${a['grandparent_title']} - ${a['title']}
${a['title']}
% endif
</div>
</div>
<div id="stream-${a['session_key']}" class="collapse out">
<div class='dashboard-activity-info-details-overlay'>
<div class='dashboard-activity-info-details-content'>
% if a['type'] == 'track':
Artist: <strong>${a['grandparent_title']}</strong>
<br>
Album: <strong>${a['parent_title']}</strong>
<br>
% endif
% if a['state'] == 'playing':
State: <strong>Playing</strong>
% elif a['state'] == 'paused':
State: <strong>Paused</strong>
% elif a['state'] == 'buffering':
State: <strong>Buffering</strong>
% endif
<br>
% if a['type'] == 'track':
% if a['audio_decision'] == 'direct play':
Stream: <strong>Direct Play</strong>
<div class="dashboard-activity-metadata-subtitle">
% if a['type'] == 'episode':
S${a['parent_media_index']} &middot; E${a['media_index']}
% elif a['type'] == 'movie':
${a['year']}
% elif a['type'] == 'track':
<a href="info?item_id=${a['parent_rating_key']}">${a['parent_title']}</a>
% elif a['type'] == 'photo':
${a['title']}
% else:
Stream: <strong>Transcoding</strong>
${a['year']}
% endif
<br/>
% if a['audio_decision'] != 'direct play':
Audio: <strong>${a['transcode_audio_codec']} (${a['transcode_audio_channels']}ch)</strong>
% elif a['audio_decision'] == 'direct play':
Audio: <strong>${a['audio_codec']} (${a['audio_channels']}ch)</strong>
% endif
% elif a['type'] == 'episode' or a['type'] == 'movie' or a['type'] == 'clip':
% if a['video_decision'] == 'direct play':
Stream: <strong>Direct Play</strong>
</div>
<div class="dashboard-activity-metadata-user">
% if a['user_id']:
<a href="user?user_id=${a['user_id']}">${a['friendly_name']}</a>
% else:
Stream: <strong>Transcoding</strong>
<a href="user?user=${a['user']}">${a['friendly_name']}</a>
% endif
<br/>
% if a['video_decision'] != 'direct play':
Video: <strong>${a['video_decision']} (${a['transcode_video_codec']})
(${a['transcode_width']}x${a['transcode_height']})</strong>
% elif a['audio_decision'] == 'direct play':
Video: <strong>${a['video_decision']} (${a['video_codec']})
(${a['width']}x${a['height']})</strong>
% endif
<br/>
% if a['audio_decision'] != 'direct play':
Audio: <strong>${a['audio_decision']} ${a['transcode_audio_codec']} (${a['transcode_audio_channels']}ch)</strong>
% elif a['audio_decision'] == 'direct play':
Audio: <strong>${a['audio_codec']} (${a['audio_channels']}ch)</strong>
% endif
% endif
<br>
</div>
</div>
</div>
</div>
</div>
<div class="dashboard-activity-button-info">
<button type="button" class="btn btn-activity-info btn-sm" data-toggle="collapse" data-target="#stream-${a['session_key']}">
<i class='fa fa-info-circle'></i>
</button>
</div>
</div>
<script>
$("#platform-${a['session_key']}").html("<img src='" + getPlatformImagePath('${a['platform']}') + "'>");
$("#platform-${a['session_key']}").prepend("<div class='dashboard-activity-info-platform-box' style='background-image: url(" + getPlatformImagePath('${a['platform']}') + ");'>");
</script>
% endfor
<script>
// When using bif indexes make the image transition a little smoother.
function fadeIn(obj) {
$(obj).fadeIn(450);
}
$('.bif').each(function() {
$(this).hide().fadeIn(1000);
});
// Convert timestamps to readable times
$('.progress_time').each(function(index) {
$(this).html(millisecondsToMinutes($(this).text(), false));
});
// Hide the info bar on page load
$('.dashboard-activity-poster-info-bar').hide();
// When mouse over the activity pane, show an info bar with extra info.
$('.dashboard-activity-poster-face').mouseenter(function() {
$('.dashboard-activity-poster-info-bar', this).slideDown('fast');
});
$('.dashboard-activity-poster-face').mouseleave(function() {
$('.dashboard-activity-poster-info-bar', this).slideUp('fast');
});
// Tooltips
$('.dashboard-activity-metadata-platform').each(function() {
$(this).tooltip();
// Show/Hide activity info
$('.btn-activity-info').on('click', function(e) {
e.preventDefault();
$($(this).attr('data-target')).toggle();
});
</script>
% else:

View File

@@ -74,6 +74,24 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div id="confirm-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Purge</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to purge all history for this user?</p>
<p>This is permanent and cannot be undone!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-purge">Purge</button>
</div>
</div>
</div>
</div>
<script>
// Set new friendly name
$("#save_user_name").click(function() {
@@ -119,9 +137,9 @@ 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) {
$("#delete-all-history").on('click', function() {
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-purge', function () {
$.ajax({
url: 'delete_all_user_history',
data: {user_id: '${data['user_id']}'},
@@ -131,7 +149,34 @@ DOCUMENTATION :: END
location.reload();
}
});
}
});
});
$(document).ready(function() {
// Move #confirm-modal to parent container
if(!($('#edit-user-modal').next().is('#confirm-modal'))) {
$('#confirm-modal').appendTo($('#edit-user-modal').parent()); }
$('#edit-user-modal > #confirm-modal').remove();
$('#edit-user-modal').css('z-index', '1050');
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1049');
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
$('#confirm-modal').on('show.bs.modal', function () {
// Fix position to match parent modal
var currentPadding = parseInt($('body').css('padding-right'));
$(this).children('.modal-dialog').css('left', -currentPadding/2);
$('#edit-user-modal').css('overflow-y', 'hidden');
});
$('#confirm-modal').on('shown.bs.modal', function () {
$(this).css('z-index', '1060');
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1059');
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
});
$('#confirm-modal').on('hidden.bs.modal', function() {
$('body').addClass('modal-open');
$('#edit-user-modal').css('overflow-y', 'auto');
});
});
</script>

View File

@@ -48,7 +48,7 @@
<div class="col-md-12">
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The total play count or duration of movies and tv played per day. Click a graph point to open up a list of items played for that specific date.
The total play count or duration of tv, movies, and music played per day. Click a graph point to open up a list of items played for that specific date.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_day">
@@ -62,7 +62,7 @@
<div class="col-md-6">
<h4><i class="fa fa-calendar"></i> <span class="yaxis-text">Play count</span> by day of week <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv played per day of the week.
The combined total of tv, movies, and music played per day of the week.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_dayofweek" style="float: left;">
@@ -75,7 +75,7 @@
<div class="col-md-6">
<h4><i class="fa fa-clock-o"></i> <span class="yaxis-text">Play count</span> by hour of day <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv played per hour of the day.
The combined total of tv, movies, and music played per hour of the day.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_hourofday">
@@ -90,7 +90,7 @@
<div class="col-md-6">
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by top 10 platforms <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv played by top 10 most active platforms.
The combined total of tv, movies, and music played by top 10 most active platforms.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_platform" style="float: left;">
@@ -103,7 +103,7 @@
<div class="col-md-6">
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by top 10 users <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv played by top 10 most active users.
The combined total of tv, movies, and music played by top 10 most active users.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_user">
@@ -121,7 +121,7 @@
<div class="col-md-12">
<h4><i class="fa fa-video-camera"></i> Daily Stream type breakdown <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The total play count or duration of movies and tv by the transcode decision. Click a graph point to open up a list of items played for that specific date.
The total play count or duration of tv, movies, and music by the transcode decision. Click a graph point to open up a list of items played for that specific date.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_stream_type">
@@ -135,7 +135,7 @@
<div class="col-md-6">
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by source resolution <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv by their original resolution (pre-transcoding).
The combined total of tv and movies by their original resolution (pre-transcoding).
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_source_resolution" style="float: left;">
@@ -148,7 +148,7 @@
<div class="col-md-6">
<h4><i class="fa fa-expand"></i> <span class="yaxis-text">Play count</span> by stream resolution <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv by their streamed resolution (post-transcoding).
The combined total of tv and movies by their streamed resolution (post-transcoding).
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_stream_resolution">
@@ -163,7 +163,7 @@
<div class="col-md-6">
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by platform and stream type <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv by platform and stream type.
The combined total of tv, movies, and music by platform and stream type.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_platform_by_stream_type" style="float: left;">
@@ -176,7 +176,7 @@
<div class="col-md-6">
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by user and stream type <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of movies and tv by user and stream type.
The combined total of tv, movies, and music by user and stream type.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_user_by_stream_type" style="float: left;">
@@ -194,7 +194,7 @@
<div class="col-md-12">
<h4><i class="fa fa-calendar"></i> Plays by Month <small>Last 12 months</small></h4>
<p class="help-block">
The combined total of movies and tv by month.
The combined total of tv, movies, and music by month.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_month">

View File

@@ -14,8 +14,11 @@
<span><i class="fa fa-history"></i> History</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>&nbsp
<div class="colvis-button-bar hidden-xs"></div>
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
<i class="fa fa-trash-o"></i> Delete mode
</button>
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect rows to delete. Data is deleted upon exiting delete mode.</div>
</div>
</div>
<div class='table-card-back'>
@@ -42,6 +45,24 @@
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Delete</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
<p>This is permanent and cannot be undone!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Delete</button>
</div>
</div>
</div>
</div>
</div>
</div>
</%def>
@@ -68,18 +89,42 @@
clearSearchButton('history_table', history_table);
$('#row-edit-mode').click(function() {
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-delete', function () {
for (var i = 0; i < history_to_delete.length; i++) {
$.ajax({
url: 'delete_history_rows',
data: { row_id: history_to_delete[i] },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
}
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
});
</script>
</%def>

View File

@@ -27,7 +27,8 @@
<div class="modal-footer"></div>
</div>
</div>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<script src="interfaces/default/js/tables/history_table_modal.js"></script>
<script>
$(document).ready(function() {
@@ -45,6 +46,32 @@
history_table = $('#history_table').DataTable(history_table_modal_options);
clearSearchButton('history_table', history_table);
// Move #info-modal to parent container
if (!($('#history-modal').next().is('#info-modal'))) {
$('#info-modal').appendTo($('#history-modal').parent());
}
$('#history-modal > #info-modal').remove();
$('#history-modal').css('z-index', '1050');
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1049');
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
$('#info-modal').on('show.bs.modal', function () {
// Fix position to match parent modal
var currentPadding = parseInt($('body').css('padding-right'));
$(this).children('.modal-dialog').css('left', -currentPadding / 2);
$('#history-modal').css('overflow-y', 'hidden');
setTimeout(function () {
$('#info-modal').css('z-index', '1060');
$('.modal-backdrop').not('.modal-backdrop-stack').css('z-index', '1059');
$('.modal-backdrop').not('.modal-backdrop-stack').addClass('modal-backdrop-stack');
}, 0);
});
$('#info-modal').on('hidden.bs.modal', function () {
$('body').addClass('modal-open');
$('#history-modal').css('overflow-y', 'auto');
});
});
</script>
% else:

View File

@@ -17,6 +17,9 @@ data[array_index]['rows'] :: Usable parameters
row_id Return the db row id for a metadata item if one exists
== Only if 'stat_id' is 'top_tv' or 'popular_tv' or 'top_movies' or 'popular_movies' or 'last_watched' ==
thumb Return the thumb for the media item.
== Only if 'stat_id' is 'top_tv' or 'popular_tv' ==
grandparent_thumb Returns location of the item's thumbnail. Use with pms_image_proxy.
rating_key Returns the unique identifier for the media item.
@@ -29,15 +32,18 @@ total_duration Returns the total duration for the associated stat.
== 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' ==
thumb Returns url of the user's gravatar. Returns '' if none exists.
== Only if 'stat_id' is 'top_user' or 'last_watched' ==
user_thumb Returns url of the user's gravatar. Returns '' if none exists.
user Returns the username for the associated stat.
user_id Returns the user id for the associated stat.
friendly_name Returns the friendly name of the user for the associated stat.
== Only if 'stat_id' is 'top_platform' ==
== Only if 'stat_id' is 'top_platform' or 'last_watched' ==
platform_type Returns the platform name for the associated stat.
== Only if 'stat_id' is 'last_watched' ==
last_watch Returns the time the media item was last watched.
DOCUMENTATION :: END
</%doc>
@@ -45,212 +51,601 @@ DOCUMENTATION :: END
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)
def hd(seconds):
minutes = helpers.cast_to_float(seconds) / 60
if minutes > 60:
hours = int(minutes / 60)
minutes = int(minutes % 60)
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>"
return "<h3>" + str(int(minutes)) + "</h3><p>mins</p>"
%>
% if data:
% if data[0]['rows'] or data[2]['rows']:
% if data[0]['rows'] or data[1]['rows'] or data[2]['rows'] or data[3]['rows'] or data[4]['rows'] or data[5]['rows']:
<ul class="list-unstyled">
% for a in data:
% if a['stat_id'] == 'top_tv' and a['rows']:
% for top_stat in data:
% if top_stat['stat_id'] == 'top_tv' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Watched TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3>
<div class="home-platforms-instance-playcount">
<h4>
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
${top_stat['rows'][0]['title']}
</a>
</h4>
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
${top_stat['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb']:
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
% if top_stat['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 class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['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 class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
${top_stat['rows'][loop.index]['title']}
</a>
</h5>
</div>
<div class="home-platforms-instance-list-playcount">
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
<p> plays</p>
% else:
${top_stat['rows'][loop.index]['total_duration'] | hd}
% endif
</div>
</div>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
% if top_stat['rows'][loop.index]['grandparent_thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster2">
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% elif a['stat_id'] == 'popular_tv' and a['rows']:
% elif top_stat['stat_id'] == 'popular_tv' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Popular TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['users_watched']}</h3>
<div class="home-platforms-instance-playcount">
<h4>
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
${top_stat['rows'][0]['title']}
</a>
</h4>
<h3>${top_stat['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'] != '':
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
% if top_stat['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 class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['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 class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
${top_stat['rows'][loop.index]['title']}
</a>
</h5>
</div>
<div class="home-platforms-instance-list-playcount">
<h3>${top_stat['rows'][loop.index]['users_watched']}</h3>
<p> users</p>
</div>
</div>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
% if top_stat['rows'][loop.index]['grandparent_thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['grandparent_thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster2">
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% elif a['stat_id'] == 'top_movies' and a['rows']:
% elif top_stat['stat_id'] == 'top_movies' and top_stat['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>
<div class="home-platforms-instance-playcount">
<h4>
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
${top_stat['rows'][0]['title']}
</a>
</h4>
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
${top_stat['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['thumb']:
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
% if top_stat['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 class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['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 class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
${top_stat['rows'][loop.index]['title']}
</a>
</h5>
</div>
<div class="home-platforms-instance-list-playcount">
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
<p> plays</p>
% else:
${top_stat['rows'][loop.index]['total_duration'] | hd}
% endif
</div>
</div>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
% if top_stat['rows'][loop.index]['thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster2">
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% elif a['stat_id'] == 'popular_movies' and a['rows']:
% elif top_stat['stat_id'] == 'popular_movies' and top_stat['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>
<div class="home-platforms-instance-playcount">
<h4>
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
${top_stat['rows'][0]['title']}
</a>
</h4>
<h3>${top_stat['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']:
<a href="info?item_id=${top_stat['rows'][0]['rating_key']}">
% if top_stat['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 class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['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 class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
${top_stat['rows'][loop.index]['title']}
</a>
</h5>
</div>
<div class="home-platforms-instance-list-playcount">
<h3>${top_stat['rows'][loop.index]['users_watched']}</h3>
<p> users</p>
</div>
</div>
<a href="info?item_id=${top_stat['rows'][loop.index]['rating_key']}">
% if top_stat['rows'][loop.index]['thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster2">
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% elif a['stat_id'] == 'top_users' and a['rows']:
% elif top_stat['stat_id'] == 'top_users' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Active User</h4>
<h5>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
</div>
<div class="home-platforms-instance-playcount">
<h4>
% if top_stat['rows'][0]['user_id']:
<a href="user?user_id=${top_stat['rows'][0]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
<a href="user?user=${top_stat['rows'][0]['user']}">
% endif
${a['rows'][0]['friendly_name']}
${top_stat['rows'][0]['friendly_name']}
</a>
</h4>
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
${top_stat['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
% if top_stat['rows'][0]['user_id']:
<a href="user?user_id=${top_stat['rows'][0]['user_id']}">
% else:
<a href="user?user=${top_stat['rows'][0]['user']}">
% endif
% if top_stat['rows'][0]['user_thumb'] != '':
<div class="home-platforms-instance-poster">
<div class="home-platforms-instance-oval" style="background-image: url(${top_stat['rows'][0]['user_thumb']});"></div>
</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>
</div>
% endif
</a>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
% if top_stat['rows'][loop.index]['user_id']:
<a href="user?user_id=${top_stat['rows'][loop.index]['user_id']}">
% else:
<a href="user?user=${top_stat['rows'][loop.index]['user']}">
% endif
${top_stat['rows'][loop.index]['friendly_name']}
</a>
</h5>
</div>
<div class="user-platforms-instance-playcount">
% if a['stat_type'] == 'total_plays':
<h3>${a['rows'][0]['total_plays']}</h3>
<div class="home-platforms-instance-list-playcount">
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
<p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
${top_stat['rows'][loop.index]['total_duration'] | hd}
% endif
</div>
</div>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% if top_stat['rows'][loop.index]['user_id']:
<a href="user?user_id=${top_stat['rows'][loop.index]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
<a href="user?user=${top_stat['rows'][loop.index]['user']}">
% endif
% if a['rows'][0]['thumb'] != '':
% if top_stat['rows'][loop.index]['user_thumb'] != '':
<div class="home-platforms-instance-poster">
<div class="home-platforms-instance-oval" style="background-image: url(${a['rows'][0]['thumb']});">
<div class="home-platforms-instance-list-oval" style="background-image: url(${top_stat['rows'][loop.index]['user_thumb']});"></div>
</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 class="home-platforms-instance-list-oval" style="background-image: url(interfaces/default/images/gravatar-default.png);"></div>
</div>
% endif
</a>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% elif a['stat_id'] == 'top_platforms' and a['rows']:
% elif top_stat['stat_id'] == 'top_platforms' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<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>
<div class="home-platforms-instance-playcount">
<h4>${top_stat['rows'][0]['platform_type']}</h4>
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][0]['total_plays']}</h3>
<p> plays</p>
% else:
${a['rows'][0]['total_duration'] | hd}
${top_stat['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);">
<script>
$("#platform-stat").html("<div class='home-platforms-instance-box' style='background-image: url(" + getPlatformImagePath('${top_stat['rows'][0]['platform_type']}') + ");'>");
</script>
</div>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
${top_stat['rows'][loop.index]['platform_type']}
</h5>
</div>
<div class="home-platforms-instance-list-playcount">
% if top_stat['stat_type'] == 'total_plays':
<h3>${top_stat['rows'][loop.index]['total_plays']}</h3>
<p> plays</p>
% else:
${top_stat['rows'][loop.index]['total_duration'] | hd}
% endif
</div>
</div>
<div class="home-platforms-instance-poster" id="home-platforms-instance-poster-${loop.index + 1}">
<script>
$("#home-platforms-instance-poster-${loop.index + 1}").html("<div class='home-platforms-instance-list-box' style='background-image: url(" + getPlatformImagePath('${top_stat['rows'][loop.index]['platform_type']}') + ");'>");
</script>
</div>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% elif top_stat['stat_id'] == 'last_watched' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Last Watched</h4>
</div>
<div class="home-platforms-instance-last-user">
<h4>
<a href="info?source=history&item_id=${top_stat['rows'][0]['row_id']}">
${top_stat['rows'][0]['title']}
</a>
</h4>
<h5>
% if top_stat['rows'][0]['user_id']:
<a href="user?user_id=${top_stat['rows'][0]['user_id']}">
% else:
<a href="user?user=${top_stat['rows'][0]['user']}">
% endif
${top_stat['rows'][0]['friendly_name']}
</a>
</h5>
<p>
<span id="last-watch-stat">
<script>
$("#platform-stat").html("<div class='home-platforms-instance-box' style='background-image: url(" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + ");'>");
$('#last-watch-stat').text(moment(${top_stat['rows'][0]['last_watch']},"X").format(date_format));
</script>
</span> - ${top_stat['rows'][0]['platform_type']}
</p>
</div>
</div>
<a href="info?source=history&item_id=${top_stat['rows'][0]['row_id']}">
% if top_stat['rows'][0]['thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][0]['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="home-platforms-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
%if len(top_stat['rows']) > 1:
<div class="home-platforms-instance-list-chevron"><i class="fa fa-chevron-down"></i></div>
<ul class="list-unstyled">
<div class="slider">
<div class="home-platforms-instance-list">
% for row in top_stat['rows']:
%if loop.index > 0:
<li>
<div class="home-platforms-instance-list-info">
<div class="home-platforms-instance-list-name">
<h5>
<a href="info?source=history&item_id=${top_stat['rows'][loop.index]['row_id']}">
${top_stat['rows'][loop.index]['title']}
</a>
</h5>
</div>
<div class="home-platforms-instance-list-last-user">
<h5>
% if top_stat['rows'][loop.index]['user_id']:
<a href="user?user_id=${top_stat['rows'][loop.index]['user_id']}">
% else:
<a href="user?user=${top_stat['rows'][loop.index]['user']}">
% endif
${top_stat['rows'][loop.index]['friendly_name']}
</a>
</h5>
<p>
<span id="home-platforms-instance-list-last-watch-${loop.index + 1}">
<script>
$('#home-platforms-instance-list-last-watch-${loop.index + 1}').text(moment(${top_stat['rows'][loop.index]['last_watch']},"X").format(date_format));
</script>
</span> - ${top_stat['rows'][loop.index]['platform_type']}
</p>
</div>
</div>
<a href="info?source=history&item_id=${top_stat['rows'][loop.index]['row_id']}">
% if top_stat['rows'][loop.index]['thumb']:
<div class="home-platforms-instance-list-poster">
<div class="home-platforms-list-poster-face" style="background-image: url(pms_image_proxy?img=${top_stat['rows'][loop.index]['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster2">
<div class="home-platforms-list-poster-face" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</a>
<div class="home-platforms-instance-list-number">
<h4>${loop.index + 1}</h4>
</div>
</li>
% endif
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% endif
% endfor
</ul>
<script>
var topZIndex = 2;
$('.home-platforms-instance-list-chevron').on('click', function() {
var instanceBoxChevron = $(this);
var instanceBox = $(this).parents('.home-platforms-instance');
var instanceBoxSlider = instanceBox.find('.slider');
topZIndex++;
instanceBoxChevron.toggleClass('active');
instanceBoxSlider.css('z-index', topZIndex);
instanceBoxSlider.stop().slideToggle(500);
});
</script>
% else:
<div class="text-muted">No stats for selected period.</div><br>
% endif

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="3086px" height="1000px" viewBox="0 0 3086 1000" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->
<title>plex-logo-light</title>
<desc>Created with Sketch.</desc>
<defs>
<radialGradient cx="89.2670157%" cy="49.76%" fx="89.2670157%" fy="49.76%" r="92.4996161%" id="radialGradient-1">
<stop stop-color="#F9BE03" offset="0%"></stop>
<stop stop-color="#CC7C19" offset="100%"></stop>
</radialGradient>
</defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="plex-logo-light" sketch:type="MSArtboardGroup">
<g sketch:type="MSLayerGroup">
<path d="M3085.99,0 L2795.989,0 L2505.99,500 L2795.989,1000 L3085.737,1000 L2795.989,500.25 L3085.99,0" id="X" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M2186,0 L2476.00071,0 L2796,500.25 L2476.00071,1000.5 L2186,1000.5 L2505.99929,500.25 L2186,0" id="chevron" fill="url(#radialGradient-1)" sketch:type="MSShapeGroup"></path>
<path d="M2085.947,1000 L1508.874,1000 L1508.874,0 L2085.947,0 L2085.947,173.737 L1721.339,173.737 L1721.339,393.299 L2060.594,393.299 L2060.594,567.03 L1721.339,567.03 L1721.339,824.895 L2085.947,824.895 L2085.947,1000" id="E" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<path d="M791.276,1000 L791.276,0 L1003.316,0 L1003.316,824.895 L1408.925,824.895 L1408.925,1000 L791.276,1000" id="L" fill="#FFFFFF" sketch:type="MSShapeGroup"></path>
<g id="P" fill="#FFFFFF" sketch:type="MSShapeGroup">
<path d="M589.947,558.824 C522.679,615.831 427.037,644.325 303.009,644.325 L212.04,644.325 L212.04,1000 L0,1000 L0,643.947829 L0,470.337388 L290,470.697418 C467.563171,468.627777 476.842468,359.878967 476.842468,322.200012 C476.842468,287.221283 476.842468,175.445374 319,173.699997 L0,173.703242 L0,0 L319.424,0 C440.717,0 532.939,26.107 596.101,78.321 C659.253,130.534 690.834,208.392 690.834,311.902 C690.834,419.527 657.202,501.83 589.947,558.824 Z" id="Path"></path>
<rect id="Path" x="0" y="110" width="212.2" height="429"></rect>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@@ -19,7 +19,7 @@
<div class="row">
<div class="col-md-12">
<div class="padded-header">
<h3>Statistics <small>Last ${config['home_stats_length']} days</small></h3>
<h3>Watch Statistics <small>Last ${config['home_stats_length']} days</small></h3>
</div>
<div id="home-stats" class="home-platforms">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
@@ -27,6 +27,17 @@
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="padded-header" id="library-statistics-header">
<h3>Library Statistics</h3>
</div>
<div id="library-stats" class="library-platforms">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
<br>
</div>
</div>
</div>
<div class='row'>
<div class="col-md-12">
<div class="padded-header">
@@ -45,17 +56,18 @@
<script src="interfaces/default/js/moment-with-locale.js"></script>
<script>
function getHomeStats(days, plays) {
function currentActivityHeader() {
$.ajax({
url: 'home_stats',
url: 'get_current_activity_header',
cache: false,
async: true,
data: {time_range: days, stat_type: plays},
complete: function(xhr, status) {
$("#home-stats").html(xhr.responseText);
$("#current-activity-header").html(xhr.responseText);
}
});
}
currentActivityHeader();
setInterval(currentActivityHeader, 15000);
function currentActivity() {
$.ajax({
@@ -70,18 +82,50 @@
currentActivity();
setInterval(currentActivity, 15000);
function currentActivityHeader() {
function getHomeStats(days, stat_type, stat_count) {
$.ajax({
url: 'get_current_activity_header',
url: 'home_stats',
cache: false,
async: true,
data: {time_range: days, stat_type: stat_type, stat_count: stat_count},
complete: function(xhr, status) {
$("#current-activity-header").html(xhr.responseText);
$("#home-stats").html(xhr.responseText);
}
});
}
function getLibraryStatsHeader() {
$.ajax({
"url": "get_servers_info",
type: "post",
cache: false,
async: true,
data: { },
complete: function (xhr, status) {
server_info = $.parseJSON(xhr.responseText);
var server_name = 'Server name not found';
for (var i in server_info) {
if (server_info[i].machine_identifier == '${config['pms_identifier']}') {
server_name = server_info[i].name
break;
}
}
$('#library-statistics-header h3').append(' <small>' + server_name + '</small>')
}
});
}
function getLibraryStats() {
$.ajax({
url: 'library_stats',
cache: false,
async: true,
data: { },
complete: function(xhr, status) {
$("#library-stats").html(xhr.responseText);
}
});
}
currentActivityHeader();
setInterval(currentActivityHeader, 15000);
function recentlyAdded() {
var widthVal = $('body').find(".container-fluid").width();
@@ -110,7 +154,20 @@
});
});
getHomeStats(${config['home_stats_length']}, ${config['home_stats_type']});
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
$.ajax({
url: 'get_date_formats',
type: 'GET',
success: function(data) {
date_format = data.date_format;
time_format = data.time_format;
}
});
getHomeStats(${config['home_stats_length']}, ${config['home_stats_type']}, ${config['home_stats_count']});
getLibraryStatsHeader();
getLibraryStats();
</script>

View File

@@ -47,67 +47,159 @@ DOCUMENTATION :: END
% if data:
<div class="container-fluid">
<div class="row">
<div>
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)">
<div class="summary-wrapper">
<div class="summary-overlay">
<div class="row">
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
<div class="summary-container">
<div class="summary-navbar">
<div class="col-md-12">
<div class="summary-navbar-list">
% if data['type'] == 'movie':
<span>Movies</span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">${data['title']}</a></span>
% elif data['type'] == 'show':
<span>TV Shows</span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">${data['title']}</a></span>
% elif data['type'] == 'season':
<span class="hidden-xs hidden-sm">TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">Season ${data['index']}</a></span>
% elif data['type'] == 'episode':
<span class="hidden-xs hidden-sm">TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">Episode ${data['index']} - ${data['title']}</a></span>
% elif data['type'] == 'artist':
<span>Music</span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">${data['title']}</a></span>
% elif data['type'] == 'album':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">${data['title']}</a></span>
% elif data['type'] == 'track':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="#">Track ${data['index']} - ${data['title']}</a></span>
% endif
</div>
</div>
</div>
<div class="summary-content-title-wrapper">
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% 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>
% if data['type'] == 'track':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web">
% else:
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"></div>
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web">
% endif
% if data['type'] == 'episode':
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
% elif data['type'] == 'artist' or data['type'] == 'album' or data['type'] == 'track':
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
% else:
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
% endif
</a>
</div>
<div class="summary-content">
<div class="summary-content-title">
% if data['type'] == 'movie':
<h1>${data['title']}</h1>
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'artist':
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['type'] == 'season':
<h1>${data['parent_title']} (${data['title']})</h1>
<h1>&nbsp;</h1><h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
<h3 class="hidden-xs">S${data['index']}</h3>
% elif data['type'] == 'episode':
<h1>${data['grandparent_title']} - ${data['title']}
(Season ${data['parent_index']}, Episode ${data['index']})</h1>
% else:
<h1>${data['title']}</h1>
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
<h2>${data['title']}</h2>
<h3 class="hidden-xs">S${data['parent_index']} &middot; E${data['index']}</h3>
% elif data['type'] == 'album':
<h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
<h2>${data['title']}</h2>
% elif data['type'] == 'track':
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
<h2><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2>
<h3 class="hidden-xs">T${data['index']}</h3>
% endif
</div>
% if (data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode') and data['rating']:
</div>
</div>
<div class="summary-content-wrapper">
<div class="col-md-9">
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div>
% elif data['type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 40px;"></div>
% elif data['type'] == 'artist' or data['type'] == 'album' or data['type'] == 'track':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;"></div>
% else:
<div class="summary-content-padding hidden-xs hidden-sm"></div>
% endif
<div class="summary-content">
<div class="summary-content-details-wrapper">
% if 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') and data['directors']:
<div class="summary-content-details-tag">
% if data['directors']:
Directed by <strong> ${data['directors'][0]}</strong>
% endif
</div>
<div class="summary-content-studio">
% if (data['type'] == 'show' or data['type'] == 'movie') and data['studio']:
<div class="summary-content-details-tag">
% if data['studio']:
Studio <strong> ${data['studio']}</strong>
% endif
</div>
<div class="summary-content-airdate">
<div class="summary-content-details-tag">
% 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>
% elif data['type'] == 'album' or data['type'] == 'track':
Released <strong> ${data['year']}</strong>
% endif
</div>
<div class="summary-content-duration">
<div class="summary-content-details-tag">
% if data['duration']:
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
% endif
</div>
<div class="summary-content-content-rating">
% if (data['type'] == 'episode' or data['type'] == 'movie' or data['type'] == 'show') and data['content_rating']:
<div class="summary-content-details-tag">
% if data['content_rating']:
Rated <strong> ${data['content_rating']} </strong>
% endif
</div>
</div>
% if data['tagline']:
<div class="summary-content-summary">
<p><strong> ${data['tagline']} </strong></p>
</div>
% endif
<div class="summary-content-summary">
<p> ${data['summary']} </p>
</div>
@@ -115,21 +207,7 @@ DOCUMENTATION :: END
</div>
<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']:
% if data['writers']:
<div class="summary-content-writers">
<strong>Written by</strong>
<ul>
@@ -143,7 +221,7 @@ DOCUMENTATION :: END
</ul>
</div>
% endif
% if (data['type'] == 'movie' or data['type'] == 'show') and data['actors']:
% if data['actors']:
<div class="summary-content-actors">
<strong>Starring</strong>
<ul>
@@ -158,25 +236,79 @@ DOCUMENTATION :: END
</div>
% endif
</div>
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
% if 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
</div>
</div>
% if data['type'] == 'show':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Season List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"></div>
</div>
</div>
% elif data['type'] == 'season':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Episode List for <strong>${data['title']}</strong></span>
</div>
</div>
% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show':
<div class='container-fluid'>
<div class='row'>
<div class='table-card-back'>
<div id="children-list"></div>
</div>
</div>
% elif data['type'] == 'artist':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Album List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"></div>
</div>
</div>
% elif data['type'] == 'album':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Track List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"></div>
</div>
</div>
% endif
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Watch History for <strong>${data['title']}</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>&nbsp
<div class="colvis-button-bar hidden-xs"></div>
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
<i class="fa fa-trash-o"></i> Delete mode
</button>
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect rows to delete. Data is deleted upon exiting delete mode.</div>
</div>
</div>
<div class="table-card-back">
@@ -196,30 +328,33 @@ DOCUMENTATION :: END
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody>
</tbody>
<tbody></tbody>
</table>
</div>
<div id="info-modal" class="modal fade" 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 class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Delete</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
<p>This is permanent and cannot be undone!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Delete</button>
</div>
</div>
</div>
% elif data['type'] == 'season':
<div class='container-fluid'>
<div class='row'>
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
<span>Episode List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="episode-list"></div>
</div>
% endif
</div>
</div>
</div>
@@ -227,8 +362,10 @@ DOCUMENTATION :: END
<div class="container-fluid">
<div class="row">
<div class="col-md-10">
<h3>Error retrieving item data. This media may not be available in the Plex Media Server database
anymore.</h3>
<h3>
Error retrieving item data. This media may not be available in the Plex Media Server database
anymore.
</h3>
</div>
</div>
</div>
@@ -247,93 +384,114 @@ DOCUMENTATION :: END
% 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)
$('#stars').attr('data-rateit-value', starRating)
var starRating = Math.round(${data['rating']} / 2);
$('#stars').attr('data-rateit-value', starRating);
</script>
% endif
% if data['type'] == 'movie' or data['type'] == 'episode':
<script src="interfaces/default/js/tables/history_table.js"></script>
% if data['type'] == 'show' or data['type'] == 'artist':
<script>
$(document).ready(function () {
function get_history() {
history_table_options.ajax = {
"url": "get_history",
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'rating_key': ${data['rating_key']}
};
'grandparent_rating_key': ${data['rating_key']} };
}
}
history_table = $('#history_table').DataTable(history_table_options);
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':
<script src="interfaces/default/js/tables/history_table.js"></script>
% elif data['type'] == 'season' or data['type'] == 'album':
<script>
$(document).ready(function () {
function get_history() {
history_table_options.ajax = {
"url": "get_history",
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'grandparent_rating_key': ${data['rating_key']}
};
'parent_rating_key': ${data['rating_key']} };
}
}
}
</script>
% elif data['type'] == 'episode' or data['type'] == 'track' or data['type'] == 'movie':
<script>
function get_history() {
history_table_options.ajax = {
"url": "get_history",
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'rating_key': ${data['rating_key']} };
}
}
}
</script>
% endif
<script>
$(document).ready(function () {
get_history();
history_table = $('#history_table').DataTable(history_table_options);
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() {
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-delete', function () {
for (var i = 0; i < history_to_delete.length; i++) {
$.ajax({
url: 'delete_history_rows',
data: { row_id: history_to_delete[i] },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
}
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
});
</script>
% endif
% if data['type'] == 'season':
% if data['type'] == 'show' or data['type'] == 'season' or data['type'] == 'artist' or data['type'] == 'album':
<script>
$.ajax({
url: 'get_children',
url: 'get_item_children',
type: "GET",
async: true,
data: { rating_key : ${data['rating_key']} },
complete: function(xhr, status) {
$("#episode-list").html(xhr.responseText);
}
$("#children-list").html(xhr.responseText); }
});
</script>
% endif
<script>
$("#airdate").html(moment($("#airdate")).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").html(), true));
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
</script>
% endif
</%def>

View File

@@ -0,0 +1,102 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: info_children_list.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
children_type Returns the type of children in the array.
children_count Returns the number of episodes in the array.
children_list Returns an array of episodes.
data['children_list'] :: Usable paramaters
== Global keys ==
rating_key Returns the unique identifier for the media item.
index Returns the episode number.
title Returns the name of the episode.
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
parent_thumb Returns the location of the item's parent thumbnail. Use with pms_image_proxy.
DOCUMENTATION :: END
</%doc>
% if data != None:
% if data['children_count'] > 0:
<div class="item-children-wrapper">
<ul class="item-children-instance list-unstyled">
% for child in data['children_list']:
% if child['rating_key']:
% if data['children_type'] == 'track':
<li class="item-children-list-item">
% else:
<li>
% endif
<a href="info?item_id=${child['rating_key']}">
%if data['children_type'] == 'season':
<div class="item-children-poster">
% if child['thumb']:
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);">
% else:
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450);">
% endif
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
Season ${child['index']}
</div>
</div>
</div>
</div>
% elif data['children_type'] == 'episode':
<div class="item-children-poster">
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450);">
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
Episode ${child['index']}
</div>
</div>
</div>
</div>
<div class="item-children-instance-text-wrapper episode-item">
<h3>${child['title']}</h3>
</div>
% elif data['children_type'] == 'album':
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300);"></div>
</div>
<div class="item-children-instance-text-wrapper album-item">
<h3>${child['title']}</h3>
</div>
% elif data['children_type'] == 'track':
% if loop.index % 2 == 0:
<div class="item-children-list-item-even">
<span class="item-children-list-item-index">${child['index']}</span>
<span class="item-children-list-item-title">${child['title']}</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span>
</div>
% else:
<div class="item-children-list-item-odd">
<span class="item-children-list-item-index">${child['index']}</span>
<span class="item-children-list-item-title">${child['title']}</span>
<span class="item-children-list-item-duration" id="item-children-list-item-duration-${loop.index + 1}">
<script>$('#item-children-list-item-duration-${loop.index + 1}').text(moment.utc(${child['duration']}).format("m:ss"));</script>
</span>
</div>
% endif
% endif
</a>
</li>
% endif
% endfor
</ul>
</div>
% endif
% endif

View File

@@ -1,53 +0,0 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: info_episode_list.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
episode_count Returns the number of episodes in the array.
episode_list Returns an array of episodes.
data['episode_list'] :: Usable paramaters
== Global keys ==
rating_key Returns the unique identifier for the media item.
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
title Returns the name of the episode.
index Returns the episode number.
DOCUMENTATION :: END
</%doc>
% if data != None:
% if data['episode_count'] > 0:
<div class="season-episodes-wrapper">
<ul class="season-episodes-instance list-unstyled">
% for a in data['episode_list']:
<li>
<a href="info?item_id=${a['rating_key']}">
<div class="season-episodes-poster">
<div class="season-episodes-poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=410&height=230);">
<div class="season-episodes-card-overlay">
<div class="season-episodes-season">
Episode ${a['index']}
</div>
</div>
</div>
</div>
<div class="season-episodes-instance-text-wrapper">
<h3>${a['title']}</h3>
</div>
</a>
</li>
% endfor
</ul>
</div>
% endif
% endif

View File

@@ -35,7 +35,7 @@ var hc_plays_by_day_options = {
}
}
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
type: 'datetime',
labels: {

View File

@@ -29,7 +29,7 @@ var hc_plays_by_dayofweek_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
categories: [{}],
labels: {
@@ -46,6 +46,24 @@ var hc_plays_by_dayofweek_options = {
style: {
color: '#aaa'
}
},
stackLabels: {
enabled: false,
style: {
color: '#fff'
}
}
},
plotOptions: {
column: {
stacking: 'normal',
borderWidth: '0',
dataLabels: {
enabled: false,
style: {
color: '#000'
}
}
}
},
tooltip: {

View File

@@ -29,7 +29,7 @@ var hc_plays_by_hourofday_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
categories: [{}],
labels: {
@@ -46,6 +46,24 @@ var hc_plays_by_hourofday_options = {
style: {
color: '#aaa'
}
},
stackLabels: {
enabled: false,
style: {
color: '#fff'
}
}
},
plotOptions: {
column: {
stacking: 'normal',
borderWidth: '0',
dataLabels: {
enabled: false,
style: {
color: '#000'
}
}
}
},
tooltip: {

View File

@@ -23,13 +23,9 @@ var hc_plays_by_month_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
type: 'datetime',
labels: {
formatter: function() {
return moment(this.value).format("MMM YYYY");
},
style: {
color: '#aaa'
}

View File

@@ -29,7 +29,7 @@ var hc_plays_by_platform_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
categories: [{}],
labels: {
@@ -46,6 +46,24 @@ var hc_plays_by_platform_options = {
style: {
color: '#aaa'
}
},
stackLabels: {
enabled: false,
style: {
color: '#fff'
}
}
},
plotOptions: {
column: {
stacking: 'normal',
borderWidth: '0',
dataLabels: {
enabled: false,
style: {
color: '#000'
}
}
}
},
tooltip: {

View File

@@ -29,7 +29,7 @@ var hc_plays_by_source_resolution_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
categories: [{}],
labels: {
@@ -46,6 +46,24 @@ var hc_plays_by_source_resolution_options = {
style: {
color: '#aaa'
}
},
stackLabels: {
enabled: false,
style: {
color: '#fff'
}
}
},
plotOptions: {
column: {
stacking: 'normal',
borderWidth: '0',
dataLabels: {
enabled: false,
style: {
color: '#000'
}
}
}
},
tooltip: {

View File

@@ -29,7 +29,7 @@ var hc_plays_by_stream_resolution_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
categories: [{}],
labels: {
@@ -46,6 +46,24 @@ var hc_plays_by_stream_resolution_options = {
style: {
color: '#aaa'
}
},
stackLabels: {
enabled: false,
style: {
color: '#fff'
}
}
},
plotOptions: {
column: {
stacking: 'normal',
borderWidth: '0',
dataLabels: {
enabled: false,
style: {
color: '#000'
}
}
}
},
tooltip: {

View File

@@ -29,7 +29,7 @@ var hc_plays_by_user_options = {
credits: {
enabled: false
},
colors: ['#F9AA03', '#FFFFFF'],
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: {
categories: [{}],
labels: {
@@ -46,6 +46,24 @@ var hc_plays_by_user_options = {
style: {
color: '#aaa'
}
},
stackLabels: {
enabled: false,
style: {
color: '#fff'
}
}
},
plotOptions: {
column: {
stacking: 'normal',
borderWidth: '0',
dataLabels: {
enabled: false,
style: {
color: '#000'
}
}
}
},
tooltip: {

View File

@@ -219,6 +219,10 @@ function getPlatformImagePath(platformName) {
return 'interfaces/default/images/platforms/playstation.png';
} else if (platformName.indexOf("Mystery 5") > -1) {
return 'interfaces/default/images/platforms/xbox.png';
} else if (platformName.indexOf("Windows") > -1) {
return 'interfaces/default/images/platforms/win8.png';
} else if (platformName.indexOf("Windows phone") > -1) {
return 'interfaces/default/images/platforms/wp.png';
} else {
return 'interfaces/default/images/platforms/default.png';
}

View File

@@ -1,5 +1,6 @@
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
var history_to_delete = [];
$.ajax({
url: 'get_date_formats',
@@ -18,7 +19,7 @@ history_table_options = {
"info":"Showing _START_ to _END_ of _TOTAL_ history items",
"infoEmpty":"Showing 0 to 0 of 0 entries",
"infoFiltered":"(filtered from _MAX_ total entries)",
"emptyTable": "No data in table",
"emptyTable": "No data in table"
},
"pagingType": "bootstrap",
"stateSave": true,
@@ -32,7 +33,7 @@ history_table_options = {
"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>');
$(td).html('<button class="btn btn-xs btn-warning" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
},
"width": "5%",
"className": "delete-control no-wrap hidden",
@@ -98,11 +99,11 @@ history_table_options = {
if (cellData !== '') {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>&nbsp';
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>&nbsp';
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>&nbsp';
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 + '&nbsp' + cellData + '</div></a></div>');
}
@@ -119,17 +120,17 @@ history_table_options = {
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>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' (' + rowData['year'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + ' \
(S' + ('00' + rowData['parent_media_index']).slice(-2) + 'E' + ('00' + rowData['media_index']).slice(-2) + ')</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' \
(S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
}
@@ -225,7 +226,7 @@ history_table_options = {
trigger: 'hover',
placement: 'right',
content: function () {
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px;" />';
}
});
@@ -238,6 +239,11 @@ history_table_options = {
"preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
showMsg(msg, false, false, 0)
},
"rowCallback": function (row, rowData) {
if ($.inArray(rowData['id'], history_to_delete) !== -1) {
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
}
}
}
@@ -287,16 +293,11 @@ $('#history_table').on('click', 'td.delete-control > button', function () {
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);
var index = $.inArray(rowData['id'], history_to_delete);
if (index === -1) {
history_to_delete.push(rowData['id']);
} else {
history_to_delete.splice(index, 1);
}
});
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
});

View File

@@ -74,6 +74,19 @@ history_table_modal_options = {
{
"targets": [3],
"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 + '&nbsp' + cellData + '</div></a></div>');
}
},
"className": "no-wrap hidden-sm hidden-xs modal-control"
},
{
@@ -81,14 +94,21 @@ history_table_modal_options = {
"data":"full_title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['media_type'] === 'movie' || rowData['media_type'] === 'episode') {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<i class="fa fa-server"></i>&nbsp';
}
$(td).html('<div><div style="float: left;"><a href="info?source=history&item_id=' + rowData['id'] + '">' + cellData + '</a></div><div style="float: right; text-align: right; padding-right: 5px;">' + transcode_dec + '<i class="fa fa-video-camera"></i></div></div>');
var media_type = '';
var thumb_popover = '';
if (rowData['media_type'] === 'movie') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' (' + rowData['year'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120">' + cellData + ' \
(S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
$(td).html('<div><div style="float: left;">' + cellData + '</div><div style="float: right; text-align: right; padding-right: 5px;"><i class="fa fa-music"></i></div></div>');
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=poster" data-height="80">' + cellData + ' (' + rowData['parent_title'] + ')</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
}
@@ -100,9 +120,40 @@ history_table_modal_options = {
// Jump to top of page
// $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.thumb-tooltip').popover({
html: true,
trigger: 'hover',
placement: 'right',
content: function () {
return '<div class="history-thumbnail" style="background-image: url(' + $(this).data('img') + '); height: ' + $(this).data('height') + 'px;" />';
}
});
},
"preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
showMsg(msg, false, false, 0)
}
}
$('#history_table').on('click', 'td.modal-control', function () {
var tr = $(this).parents('tr');
var row = history_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();
});

View File

@@ -49,7 +49,7 @@ sync_table_options = {
"data": "title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['metadata_type'] !== 'track') {
if (rowData['metadata_type'] !== '') {
$(td).html('<a href="info?item_id=' + rowData['rating_key'] + '">' + cellData + '</a>');
} else {
$(td).html(cellData);

View File

@@ -86,7 +86,7 @@ user_ip_table_options = {
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type']) {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
} else {

View File

@@ -1,3 +1,5 @@
var users_to_purge = [];
users_list_table_options = {
"language": {
"search": "Search: ",
@@ -11,44 +13,62 @@ users_list_table_options = {
"processing": false,
"serverSide": true,
"pageLength": 10,
"order": [ 1, 'asc'],
"order": [ 2, 'asc'],
"autoWidth": true,
"stateSave": true,
"pagingType": "bootstrap",
"columnDefs": [
{
"targets": [0],
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<div class="edit-user-toggles"><button class="btn btn-xs btn-warning" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&nbsp' +
'<input type="checkbox" id="do_notify-' + rowData['user_id'] + '" name="do_notify" value="1" ' + rowData['do_notify'] + '><label class="edit-tooltip" for="do_notify-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle Notifications"><i class="fa fa-bell fa-lg fa-fw"></i></label>&nbsp' +
'<input type="checkbox" id="keep_history-' + rowData['user_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label>&nbsp');
// Show/hide user currently doesn't work
//'<input type="checkbox" id="show_hide-' + rowData['user_id'] + '" name="show_hide" value="1" checked><label class="edit-tooltip" for="show_hide-' + rowData['user_id'] + '" data-toggle="tooltip" title="Show/Hide User"><i class="fa fa-eye fa-lg fa-fw"></i></label>');
},
"width": "7%",
"className": "edit-control no-wrap hidden",
"searchable": false,
"orderable": false
},
{
"targets": [1],
"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"/>');
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(interfaces/default/images/gravatar-default-80x80.png);"></div></a>');
} else {
$(td).html('<img src="' + cellData + '" alt="User Logo"/>');
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(' + cellData + ');"></div></a>');
}
},
"orderable": false,
"searchable": false,
"width": "5%",
"className": "users-poster-face"
"className": "users-thumbs"
},
{
"targets": [1],
"targets": [2],
"data": "friendly_name",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id'] > 0) {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '"><a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>' +
'<input type="text" class="hidden" value="' + cellData + '"></div>');
} else {
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>');
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '"><a href="user?user=' + rowData['user'] + '">' + cellData + '</a>' +
'<input type="text" class="hidden" value="' + cellData + '"></div>');
}
} else {
$(td).html(cellData);
}
},
"width": "15%"
"width": "12%",
"className": "edit-user-control no-wrap"
},
{
"targets": [2],
"targets": [3],
"data": "last_seen",
"render": function ( data, type, full ) {
if (data) {
@@ -58,11 +78,11 @@ users_list_table_options = {
}
},
"searchable": false,
"width": "15%",
"width": "12%",
"className": "no-wrap hidden-xs"
},
{
"targets": [3],
"targets": [4],
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
@@ -79,32 +99,32 @@ users_list_table_options = {
$(td).html('n/a');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
},
{
"targets": [4],
"targets": [5],
"data":"platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
var transcode_dec = '';
if (rowData['video_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>&nbsp';
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>&nbsp';
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>&nbsp';
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 + '&nbsp' + cellData + '</div></a></div>');
} else {
$(td).html('n/a');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [5],
"targets": [6],
"data":"last_watched",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -121,7 +141,7 @@ users_list_table_options = {
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type']) {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
} else {
@@ -129,10 +149,11 @@ users_list_table_options = {
}
}
},
"width": "30%",
"className": "hidden-sm hidden-xs"
},
{
"targets": [6],
"targets": [7],
"data": "plays",
"searchable": false,
"width": "10%"
@@ -145,6 +166,8 @@ users_list_table_options = {
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.purge-tooltip').tooltip();
$('.edit-tooltip').tooltip();
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
@@ -157,10 +180,20 @@ users_list_table_options = {
}
});
if ($('#row-edit-mode').hasClass('active')) {
$('.edit-control').each(function () {
$(this).removeClass('hidden');
});
}
},
"preDrawCallback": function(settings) {
var msg = "<div class='msg'><i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...</div>";
showMsg(msg, false, false, 0)
},
"rowCallback": function (row, rowData) {
if ($.inArray(rowData['user_id'], users_to_purge) !== -1) {
$(row).find('button[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
}
}
}
@@ -205,3 +238,51 @@ $('#users_list_table').on('click', 'td.modal-control-ip', function () {
getUserLocation(rowData['ip_address']);
});
$('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > input, td.edit-user-control > .edit-user-name > input', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
var do_notify = 0;
var keep_history = 0;
if ($('#do_notify-' + rowData['user_id']).is(':checked')) {
do_notify = 1;
}
if ($('#keep_history-' + rowData['user_id']).is(':checked')) {
keep_history = 1;
}
friendly_name = tr.find('td.edit-user-control > .edit-user-name > input').val();
$.ajax({
url: 'edit_user',
data: {
user_id: rowData['user_id'],
friendly_name: friendly_name,
do_notify: do_notify,
keep_history: keep_history,
thumb: rowData['user_thumb']
},
cache: false,
async: true,
success: function (data) {
var msg = "User updated";
showMsg(msg, false, true, 2000);
}
});
});
$('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > button', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
var index = $.inArray(rowData['user_id'], users_to_purge);
if (index === -1) {
users_to_purge.push(rowData['user_id']);
} else {
users_to_purge.splice(index, 1);
}
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
});

View File

@@ -0,0 +1,78 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: library_stats.html
Version: 0.1
Variable names: data [array]
data[array_index] :: Usable parameters
data['type'] Returns the type of the library. Either 'movie', 'show', 'photo', or 'artist'.
data['rows'] Returns an array containing stat data
data[array_index]['rows'] :: Usable parameters
title Returns the title of the library.
thumb Returns the thumb of the library.
count Returns the number of items in the library.
count_type Returns the sorting type for the library
== Only if 'type' is 'show'
episode_count Return the number of episodes in the library.
episode_count_type Return the sorting type for the episodes.
== Only if 'type' is 'artist'
album_count Return the number of episodes in the library.
album_count_type Return the sorting type for the episodes.
DOCUMENTATION :: END
</%doc>
% if data:
<ul class="list-unstyled">
% for library in data:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>${library['rows']['title']}</h4>
</div>
<div class="home-platforms-instance-playcount">
<h5>${library['rows']['count_type']}</h5>
<h3>${library['rows']['count']}</h3>
<p> items</p>
</div>
% if library['type'] == 'show':
<div class="home-platforms-instance-playcount" style="padding-left: 10px;">
<h5>${library['rows']['episode_count_type']}</h5>
<h3>${library['rows']['episode_count']}</h3>
<p> items</p>
</div>
% endif
% if library['type'] == 'artist':
<div class="home-platforms-instance-playcount" style="padding-left: 10px;">
<h5>${library['rows']['album_count_type']}</h5>
<h3>${library['rows']['album_count']}</h3>
<p> items</p>
</div>
% endif
</div>
% if library['rows']['thumb']:
<div class="home-platforms-instance-poster">
<div class="home-platforms-library-thumb" style="background-image: url(pms_image_proxy?img=${library['rows']['thumb']}&width=300&height=300&fallback=poster);"></div>
</div>
% else:
<div class="home-platforms-instance-poster">
<div class="home-platforms-library-thumb" style="background-image: url(interfaces/default/images/poster.png);"></div>
</div>
% endif
</li>
</div>
% endfor
</ul>
% else:
<div class="text-muted">Unable to retrieve data from database. Please check your <a href="settings">settings</a>.
</div><br>
% endif

View File

@@ -31,35 +31,48 @@ DOCUMENTATION :: END
<li>
% if item['type'] == 'season' or item['type'] == 'movie':
<a href="info?item_id=${item['rating_key']}">
<div class="poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
<div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script>
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
</script>
</div>
</div>
</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season':
<h3>${item['parent_title']}</h3>
<h3>(${item['title']})</h3>
<h3 class="text-muted">${item['title']}</h3>
% elif item['type'] == 'movie':
<h3>${item['title']}</h3>
<h3>(${item['year']})</h3>
<h3 class="text-muted">${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>
<a href="info?item_id=${item['rating_key']}">
<div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
<div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script>
$('#added_at-${item['rating_key']}').text('Added ' + moment(${item['added_at']}, "X").fromNow())
</script>
</div>
</div>
</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>
<h3 class="text-muted">${item['title']}</h3>
</div>
</a>
% endif
</li>
</div>
<script>
$('#added_at-${item['rating_key']}').html('Added ' + moment(${item['added_at']}, "X").fromNow())
</script>
% endfor
</ul>
</div>

View File

@@ -83,6 +83,36 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<p class="help-block">Set your preferred time format. <a href="javascript:void(0)" data-target="#dateTimeOptionsModal" data-toggle="modal">Click here</a> to see the parameter list.</p>
</div>
<div class="padded-header">
<h3>Homepage Statistics</h3>
</div>
<div class="form-group">
<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" data-parsley-errors-container="#home_stats_length_error" required>
</div>
<div id="home_stats_length_error" class="alert alert-danger settings-alert" role="alert"></div>
</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="form-group">
<label for="home_stats_count">Top Lists</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="home_stats_count" name="home_stats_count" value="${config['home_stats_count']}" size="3" data-parsley-range="[0,10]" data-parsley-trigger="change" data-parsley-errors-container="#home_stats_count_error" required>
</div>
<div id="home_stats_count_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Specify the number of items to show in the top lists for the statistics on the home page. Max is 10 items, default is 5 items, 0 to disable.</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>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-2">
@@ -103,8 +133,9 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="http_port">HTTP Port</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="http_port" name="http_port" value="${config['http_port']}" data-parsley-trigger="change" required>
<input type="text" class="form-control" data-parsley-type="integer" id="http_port" name="http_port" value="${config['http_port']}" data-parsley-trigger="change" data-parsley-errors-container="#http_port_error" required>
</div>
<div id="http_port_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Port to bind web server to. Note that ports below 1024 may require root.</p>
</div>
@@ -204,8 +235,9 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="pms_port">Plex Port</label>
<div class="row">
<div class="col-md-2">
<input data-parsley-type="integer" class="pms-settings form-control" type="text" id="pms_port" name="pms_port" value="${config['pms_port']}" size="30" data-parsley-trigger="change" required>
<input data-parsley-type="integer" class="pms-settings form-control" type="text" id="pms_port" name="pms_port" value="${config['pms_port']}" size="30" data-parsley-trigger="change" data-parsley-errors-container="#pms_port_error" required>
</div>
<div id="pms_port_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Port that Plex Media Server is listening on.</p>
</div>
@@ -221,9 +253,24 @@ available_notification_agents = notifiers.available_notification_agents()
</label>
<p class="help-block">Force PlexPy to connect to your Plex Server via SSL. Your server needs to have remote access enabled.</p>
</div>
<div class="padded-header">
<h3>Plex Logs</h3>
</div>
<div class="form-group">
<label for="pms_logs_folder">Logs Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change">
</div>
</div>
<p class="help-block">Set the folder where your Plex Server logs are. This is required if you enable IP logging.<br /><a href="https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files" target="_blank">Click here</a> for help.</p>
</div>
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
</div>
<div role="tabpanel" class="tab-pane" id="tabs-5">
@@ -235,12 +282,13 @@ available_notification_agents = notifiers.available_notification_agents()
<div class="row">
<div class="col-md-6">
<div class="input-group">
<input type="text" class="form-control" id="pms_token" name="pms_token" value="${config['pms_token']}" data-parsley-trigger="change" required>
<input type="text" class="form-control" id="pms_token" name="pms_token" value="${config['pms_token']}" data-parsley-trigger="change" data-parsley-errors-container="#pms_token_error" required>
<span class="input-group-btn">
<button class="btn btn-form" type="button" data-toggle="modal" data-target="#pms-auth-modal">Fetch Token</button>
</span>
</div>
</div>
<div id="pms_token_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Token for Plex.tv authentication.</p>
</div>
@@ -252,8 +300,9 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="refresh_users_interval">User list Refresh Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_users_interval" name="refresh_users_interval" value="${config['refresh_users_interval']}" size="5" data-parsley-range="[1,24]" data-parsley-trigger="change" required>
<input type="text" class="form-control" data-parsley-type="integer" id="refresh_users_interval" name="refresh_users_interval" value="${config['refresh_users_interval']}" size="5" data-parsley-range="[1,24]" data-parsley-trigger="change" data-parsley-errors-container="#refresh_users_interval_error" required>
</div>
<div id="refresh_users_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in hours) PlexPy will request an updated friends list from Plex.tv. 1 minimum, 24 maximum.</p>
</div>
@@ -273,46 +322,11 @@ available_notification_agents = notifiers.available_notification_agents()
<div class="checkbox">
<label>
<input type="checkbox" id="pms_use_bif" name="pms_use_bif" value="1" ${config['pms_use_bif']}> Use BIF thumbs
<input type="checkbox" id="pms_use_bif" name="pms_use_bif" value="1" ${config['pms_use_bif']}> Use video preview thumbnails (BIF)
</label>
<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">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>
</div>
</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>
</div>
<div class="form-group">
<label for="pms_logs_folder">Logs Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change">
</div>
</div>
<p class="help-block">Set the folder where your Plex Server logs are. This is required if you enable IP logging.<br/><a href="https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files" target="_blank">Click here</a> for help.</p>
</div>
<div class="padded-header">
<h3>PlexWatch Import Tool</h3>
</div>
@@ -329,10 +343,11 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="monitoring_interval">Monitoring Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="monitoring_interval" name="monitoring_interval" value="${config['monitoring_interval']}" size="5" data-parsley-min="30" data-parsley-trigger="change" required>
<input type="text" class="form-control" data-parsley-type="integer" id="monitoring_interval" name="monitoring_interval" value="${config['monitoring_interval']}" size="5" data-parsley-min="30" data-parsley-trigger="change" data-parsley-errors-container="#monitoring_interval_error" required>
</div>
<div id="monitoring_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in seconds) PlexPy will ping your Plex Server. Min 30 seconds, Recommended 60 seconds.</p>
<p class="help-block">The interval (in seconds) PlexPy will ping your Plex Server. Min 30 seconds, recommended 60 seconds.</p>
</div>
<div class="padded-header">
@@ -345,11 +360,12 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">Keep records of all video items played from your Plex Media Server.</p>
</div>
<div class="form-group">
<label for="monitoring_interval">Ignore Interval</label>
<label for="logging_ignore_interval">Ignore Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="logging_ignore_interval" name="logging_ignore_interval" value="${config['logging_ignore_interval']}" size="5" data-parsley-min="0" data-parsley-trigger="change" required>
<input type="text" class="form-control" data-parsley-type="integer" id="logging_ignore_interval" name="logging_ignore_interval" value="${config['logging_ignore_interval']}" size="5" data-parsley-min="0" data-parsley-trigger="change" data-parsley-errors-container="#logging_ignore_interval_error" required>
</div>
<div id="logging_ignore_interval_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in seconds) PlexPy will wait for a video item to be active before logging it. 0 to disable.</p>
</div>
@@ -381,8 +397,9 @@ 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="[0,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" data-parsley-errors-container="#buffer_threshold_error" required>
</div>
<div id="buffer_threshold_error" class="alert alert-danger settings-alert" role="alert"></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. 0 to disable buffer warnings.</p>
</div>
@@ -390,8 +407,9 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="buffer_wait">Buffer Wait</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="buffer_wait" name="buffer_wait" value="${config['buffer_wait']}" data-parsley-min="0" data-parsley-trigger="change" required>
<input type="text" class="form-control" data-parsley-type="integer" id="buffer_wait" name="buffer_wait" value="${config['buffer_wait']}" data-parsley-min="0" data-parsley-trigger="change" data-parsley-errors-container="#buffer_wait_error" required>
</div>
<div id="buffer_wait_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The value (in seconds) PlexPy should wait before triggering the next buffer warning. 0 to always trigger.</p>
</div>
@@ -422,8 +440,9 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="notify_watched_percent">Watched Percent</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_watched_percent" name="notify_watched_percent" value="${config['notify_watched_percent']}" size="5" data-parsley-range="[50,95]" data-parsley-trigger="change" required>
<input type="text" class="form-control" data-parsley-type="integer" id="notify_watched_percent" name="notify_watched_percent" value="${config['notify_watched_percent']}" size="5" data-parsley-range="[50,95]" data-parsley-trigger="change" data-parsley-errors-container="#notify_watched_percent_error" required>
</div>
<div id="notify_watched_percent_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Set the progress percentage of when a watched notification should be triggered. Minimum 50, Maximum 95.</p>
</div>
@@ -553,7 +572,7 @@ available_notification_agents = notifiers.available_notification_agents()
<h3>Notification Agents</h3>
</div>
<p class="help-block">
Toggle the desired notification options by clicking the bolt icon and configure it by selecting the settings icon to the right.
Toggle the desired notification options by clicking the bell icon and configure it by clicking the settings icon to the right.
</p>
<br/>
<ul class="stacked-configs list-unstyled">
@@ -561,9 +580,9 @@ available_notification_agents = notifiers.available_notification_agents()
<li>
<span>
% if agent['on_play'] or agent['on_stop'] or agent['on_pause'] or agent['on_resume'] or agent['on_buffer'] or agent['on_watched']:
<a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left active" data-toggle="modal"><i class="fa fa-lg fa-flash"></i></a>
<a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left active" data-toggle="modal"><i class="fa fa-lg fa-bell"></i></a>
% else:
<a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left" data-toggle="modal"><i class="fa fa-lg fa-flash"></i></a>
<a href="javascript:void(0)" data-target="#notification-triggers-modal" data-id="${agent['id']}" class="toggle-notification-triggers-modal toggle-left" data-toggle="modal"><i class="fa fa-lg fa-bell"></i></a>
% endif
${agent['name']}
% if agent['has_config']:
@@ -1150,7 +1169,7 @@ $(document).ready(function() {
function checkLogsPath() {
if ($("#pms_logs_folder").val() == '') {
$("#debugLogCheck").html("You must first define your Plex Server Logs folder path under the Extra Settings tab.");
$("#debugLogCheck").html("You must first define your Plex Server Logs folder path under the Plex Media Server tab.");
$("#ip_logging_enable").attr("disabled", true);
} else {
$("#ip_logging_enable").attr("disabled", false);

View File

@@ -54,6 +54,7 @@ DOCUMENTATION :: END
<div class="row">
<div class="col-md-4">
<h4><strong>Stream Details</strong></h4>
% if data['media_type'] != 'track':
<h5>Video</h5>
<ul class="list-unstyled">
% if data['transcode_video_dec'] != 'direct play':
@@ -74,6 +75,7 @@ DOCUMENTATION :: END
<li>Video Height: <strong>${data['height']}</strong></li>
% endif
</ul>
% endif
<h5>Audio</h5>
<ul class="list-unstyled">
% if data['transcode_audio_dec'] != 'direct play':
@@ -91,11 +93,14 @@ DOCUMENTATION :: END
<h4><strong>Media Source Details</strong></h4>
<ul class="list-unstyled">
<li>Container: <strong>${data['container']}</strong></li>
% if data['media_type'] != 'track':
<li>Resolution: <strong>${data['height']}p</strong></li>
% endif
<li>Bitrate: <strong>${data['bitrate']} kbps</strong></li>
</ul>
</div>
<div class="col-md-4">
% if data['media_type'] != 'track':
<h4><strong>Video Source Details</strong></h4>
<ul class="list-unstyled">
<li>Width: <strong>${data['width']}</strong></li>
@@ -104,6 +109,7 @@ DOCUMENTATION :: END
<li>Video Frame Rate: <strong>${data['video_framerate']}</strong></li>
<li>Video Codec: <strong>${data['video_codec']}</strong></li>
</ul>
% endif
<h4><strong>Audio Source Details</strong></h4>
<ul class="list-unstyled">
<li>Audio Codec: <strong>${data['audio_codec']}</strong></li>

View File

@@ -40,8 +40,7 @@ from plexpy import helpers
<div class="col-md-12">
<div class="table-card-back">
<div class="user-info-wrapper">
<div class="user-info-poster-face" id="user-gravatar">
<img id="user-profile-thumb" src="${data['thumb']}" height="80px" width="80px">
<div class="user-info-poster-face" id="user-gravatar" style="background-image: url(${data['thumb']});">
</div>
<div class="user-info-username">
<span class="set-username">${data['friendly_name']}</span> <span id="edit-user-tooltip" data-target="tooltip" title="Edit user details"><a href="#" data-toggle="modal" data-target="#edit-user-modal" id="toggle-edit-user-modal"><i class="fa fa-pencil"></i></a></span>
@@ -155,9 +154,11 @@ from plexpy import helpers
</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>&nbsp
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
</div>
<div class="colvis-button-bar hidden-xs" id="button-bar-history"></div>
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
<i class="fa fa-trash-o"></i> Delete mode
</button>
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect rows to delete. Data is deleted upon exiting delete mode.</div>
</div>
</div>
<div class="table-card-back">
@@ -167,8 +168,8 @@ from plexpy import helpers
<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>
@@ -227,6 +228,24 @@ from plexpy import helpers
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Delete</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to delete <strong><span id="deleteCount"></span></strong> history item(s)?</p>
<p>This is permanent and cannot be undone!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Delete</button>
</div>
</div>
</div>
</div>
</div>
<footer></footer>
</%def>
@@ -368,14 +387,38 @@ from plexpy import helpers
});
});
// Delete mode button
$('#row-edit-mode').click(function() {
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-delete', function () {
for (var i = 0; i < history_to_delete.length; i++) {
$.ajax({
url: 'delete_history_rows',
data: { row_id: history_to_delete[i] },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
}
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}

View File

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

View File

@@ -32,29 +32,54 @@ DOCUMENTATION :: END
<div class="dashboard-recent-media-row">
<ul class="dashboard-recent-media list-unstyled">
% for item in data:
<div class="dashboard-recent-media-instance">
<li>
% if item['type'] == 'episode' or item['type'] == 'movie':
<a href="info?source=history&item_id=${item['row_id']}">
<div class="poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
<div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
<script>
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
</script>
</div>
</div>
</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'episode':
<h3>${item['parent_title']}</h3>
<h3>${item['grandparent_title']}</h3>
<h3>${item['title']}</h3>
<h3>(Season ${item['parent_index']}, Episode ${item['index']})</h3>
<h3 class="text-muted">S${item['parent_index']} &middot; E${item['index']}</h3>
% elif item['type'] == 'movie':
<h3>${item['title']}</h3>
<h3>(${item['year']})</h3>
<h3 class="text-muted">${item['year']}</h3>
% endif
<div class="text-muted" id="time-${item['time']}">${item['time']}</div>
<div class="text-muted" id="time-${item['time']}">
</div>
</div>
</a>
</li>
</div>
% elif item['type'] == 'track':
<a href="info?source=history&item_id=${item['row_id']}">
<div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">
<div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
<script>
$('#time-${item['time']}').html('Watched ' + moment(${item['time']}, "X").fromNow())
$('#time-${item['time']}').text('Watched ' + moment(${item['time']}, "X").fromNow())
</script>
</div>
</div>
</div>
</div>
<div class="dashboard-recent-media-metacontainer">
<h3>${item['grandparent_title']}</h3>
<h3>${item['title']}</h3>
<h3 class="text-muted">${item['parent_title']}</h3>
</div>
</a>
% endif
</li>
% endfor
</ul>
</div>

View File

@@ -12,13 +12,18 @@
<span><i class="fa fa-group"></i> All Users</span>
</div>
<div class="button-bar">
<button class="btn btn-dark" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
<button class="btn btn-dark refresh-users-button" id="refresh-users-list"><i class="fa fa-refresh"></i> Refresh users</button>
<button class="btn btn-danger btn-edit" data-toggle="button" aria-pressed="false" autocomplete="off" id="row-edit-mode">
<i class="fa fa-pencil"></i> Edit mode
</button>&nbsp
<div class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect users to purge. Data is purged upon exiting edit mode.</div>
</div>
</div>
<div class='table-card-back'>
<table id="users_list_table" class="display" width="100%">
<thead>
<tr>
<th align="left" id="edit_row">Edit</th>
<th align="right" id="avatar"></th>
<th align="left" id="friendly_name">User</th>
<th align="left" id="last_seen">Last Seen</th>
@@ -35,6 +40,25 @@
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
<div class="modal fade" id="confirm-modal" tabindex="-1" role="dialog" aria-labelledby="confirm-modal">
<div class="modal-dialog">
<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" id="myModalLabel">Confirm Purge</h4>
</div>
<div class="modal-body" style="text-align: center;">
<p>Are you REALLY sure you want to purge all history for the following users:</p>
<ul id="users-to-delete" class="list-unstyled"></ul>
<p>This is permanent and cannot be undone!</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-dark" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-purge">Purge</button>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -59,8 +83,63 @@
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
clearSearchButton('users_list_table', users_list_table);
$('#row-edit-mode').on('click', function () {
$('#row-edit-mode-alert').fadeIn(200);
$('#users-to-delete').html('');
if ($(this).hasClass('active')) {
if (users_to_purge.length > 0) {
$('.edit-control').each(function () {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
});
for (var i = 0; i < users_to_purge.length; i++) {
$('#users-to-delete').append('<li>' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '</li>');
}
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-purge', function () {
for (var i = 0; i < users_to_purge.length; i++) {
$.ajax({
url: 'delete_all_user_history',
data: { user_id: users_to_purge[i] },
cache: false,
async: true,
success: function (data) {
var msg = "User history purged";
showMsg(msg, false, true, 2000);
}
});
}
users_list_table.draw();
});
}
$('.edit-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
$('.edit-user-control > .edit-user-name').each(function () {
a = $(this).children('a');
input = $(this).children('input');
a.text(input.val());
a.removeClass('hidden');
input.addClass('hidden');
});
} else {
users_to_purge = [];
$('.edit-control').each(function () {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
$('.edit-user-control > .edit-user-name').each(function () {
$(this).children('a').addClass('hidden');
$(this).children('input').removeClass('hidden');
});
}
});
});
$("#refresh-users-list").click(function() {
$.ajax({

View File

@@ -1,4 +1,4 @@
# PlexPy
# PlexPy - Stats for Plex Media Server usage
#
# 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
Description=PlexPy - Stats for Plex Media Server usage
[Service]
ExecStart=/home/sabnzbd/plexpy/PlexPy.py --daemon --config /etc/plexpy/plexpy.ini --datadir /home/sabnzbd/.plexpy --nolaunch --quiet

View File

@@ -0,0 +1,67 @@
# PlexPy - Stats for Plex Media Server usage
#
# Service Unit file for systemd system manager
#
# INSTALLATION NOTES
#
# 1. Rename this file as you want, ensuring that it ends in .service
# e.g. 'plexpy.service'
#
# 2. Adjust configuration settings as required. More details in the
# "CONFIGURATION NOTES" section shown below.
#
# 3. Copy this file into your systemd service unit directory, which is
# often '/lib/systemd/system'.
#
# 4. Create any files/directories that you specified back in step #2.
# e.g. '/opt/plexpy.ini'
# '/opt/plexpy'
#
# 5. Enable boot-time autostart with the following commands:
# systemctl daemon-reload
# systemctl enable plexpy.service
#
# 6. Start now with the following command:
# systemctl start plexpy.service
#
# 7. If troubleshooting startup-errors, start by checking permissions
# and ownership on the files/directories that you created in step #4.
#
#
# CONFIGURATION NOTES
#
# - The example settings in this file assume that:
# 1. You will run PlexPy as user/group: plex.users
# 2. You will either have PlexPy installed as a subdirectory
# under '/opt', or that you will have a symlink under
# '/opt' pointing to your PlexPy install dir.
# 3. Your PlexPy data directory and configuration file can be
# in separate locations from your PlexPy install dir, to
# simplify updates. However, in the example below they are in the
# PlexPy install dir.
#
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
#
# - Adjust ExecStart= to point to:
# 1. Your PlexPy executable,
# 2. Your config file (recommended is to put it somewhere in /etc)
# 3. Your datadir (recommended is to NOT put it in your PlexPy exec dir)
#
# - Adjust User= and Group= to the user/group you want PlexPy to run as.
#
# - WantedBy= specifies which target (i.e. runlevel) to start PlexPy for.
# multi-user.target equates to runlevel 3 (multi-user text mode)
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit]
Description=PlexPy - Stats for Plex Media Server usage
[Service]
ExecStart=/opt/plexpy/PlexPy.py --daemon --config /opt/plexpy/config.ini --datadir /opt/plexpy --nolaunch --quiet
GuessMainPID=no
Type=forking
User=plex
Group=users
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,66 @@
# PlexPy - Stats for Plex Media Server usage
#
# Service Unit file for systemd system manager
#
# INSTALLATION NOTES
#
# 1. Rename this file as you want, ensuring that it ends in .service
# e.g. 'plexpy.service'
#
# 2. Adjust configuration settings as required. More details in the
# "CONFIGURATION NOTES" section shown below.
#
# 3. Copy this file into your systemd service unit directory, which is
# often '/lib/systemd/system'.
#
# 4. Create any files/directories that you specified back in step #2.
# e.g. '/etc/plexpy/plexpy.ini'
# '/home/sabnzbd/.plexpy'
#
# 5. Enable boot-time autostart with the following commands:
# systemctl daemon-reload
# systemctl enable plexpy.service
#
# 6. Start now with the following command:
# systemctl start plexpy.service
#
# 7. If troubleshooting startup-errors, start by checking permissions
# and ownership on the files/directories that you created in step #4.
#
#
# CONFIGURATION NOTES
#
# - The example settings in this file assume that:
# 1. You will run PlexPy as user/group: sabnzbd.sabnzbd
# 2. You will either have PlexPy installed as a subdirectory
# under '~sabnzbd', or that you will have a symlink under
# '~/sabnzbd' pointing to your PlexPy install dir.
# 3. Your PlexPy data directory and configuration file will be
# in separate locations from your PlexPy install dir, to
# simplify updates.
#
# - Option names (e.g. ExecStart=, Type=) appear to be case-sensitive)
#
# - Adjust ExecStart= to point to:
# 1. Your PlexPy executable,
# 2. Your config file (recommended is to put it somewhere in /etc)
# 3. Your datadir (recommended is to NOT put it in your PlexPy exec dir)
#
# - Adjust User= and Group= to the user/group you want PlexPy to run as.
#
# - WantedBy= specifies which target (i.e. runlevel) to start PlexPy for.
# multi-user.target equates to runlevel 3 (multi-user text mode)
# graphical.target equates to runlevel 5 (multi-user X11 graphical mode)
[Unit]
Description=PlexPy - Stats for Plex Media Server usage
[Service]
ExecStart=/opt/plexpy/PlexPy.py --quiet --daemon --nolaunch --config /opt/plexpy/config.ini --datadir /opt/plexpy
GuessMainPID=no
Type=forking
User=plexpy
Group=nogroup
[Install]
WantedBy=multi-user.target

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -377,7 +377,7 @@ def dbcheck():
'title TEXT, parent_title TEXT, grandparent_title TEXT, full_title TEXT, media_index INTEGER, '
'parent_media_index INTEGER, thumb TEXT, parent_thumb TEXT, grandparent_thumb TEXT, art TEXT, media_type TEXT, '
'year INTEGER, originally_available_at TEXT, added_at INTEGER, updated_at INTEGER, last_viewed_at INTEGER, '
'content_rating TEXT, summary TEXT, rating TEXT, duration INTEGER DEFAULT 0, guid TEXT, '
'content_rating TEXT, summary TEXT, tagline TEXT, rating TEXT, duration INTEGER DEFAULT 0, guid TEXT, '
'directors TEXT, writers TEXT, actors TEXT, genres TEXT, studio TEXT)'
''
)
@@ -517,15 +517,24 @@ def dbcheck():
'ALTER TABLE sessions ADD COLUMN transcode_height INTEGER'
)
# Upgrade sessions table from earlier versions
# Upgrade session_history_metadata table from earlier versions
try:
c_db.execute('SELECT full_title from session_history_metadata')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
logger.debug(u"Altering database. Updating database table session_history_metadata.")
c_db.execute(
'ALTER TABLE session_history_metadata ADD COLUMN full_title TEXT'
)
# Upgrade session_history_metadata table from earlier versions
try:
c_db.execute('SELECT tagline from session_history_metadata')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table session_history_metadata.")
c_db.execute(
'ALTER TABLE session_history_metadata ADD COLUMN tagline TEXT'
)
# notify_log table :: This is a table which logs notifications sent
c_db.execute(
'CREATE TABLE IF NOT EXISTS notify_log (id INTEGER PRIMARY KEY AUTOINCREMENT, '
@@ -588,6 +597,12 @@ def dbcheck():
'ALTER TABLE users ADD COLUMN custom_avatar_url TEXT'
)
# Add "Local" user to database as default unauthenticated user.
result = c_db.execute('SELECT id FROM users WHERE username = "Local"')
if not result.fetchone():
logger.debug(u'User "Local" does not exist. Adding user.')
c_db.execute('INSERT INTO users (user_id, username) VALUES (0, "Local")')
conn_db.commit()
c_db.close()

View File

@@ -1,4 +1,4 @@
import plexpy.logger
import plexpy.logger
import itertools
import os
import re
@@ -84,6 +84,7 @@ _CONFIG_DEFINITIONS = {
'GROWL_ON_WATCHED': (int, 'Growl', 0),
'HOME_STATS_LENGTH': (int, 'General', 30),
'HOME_STATS_TYPE': (int, 'General', 0),
'HOME_STATS_COUNT': (int, 'General', 5),
'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'),

View File

@@ -55,6 +55,7 @@ class DataFactory(object):
(CASE WHEN session_history_metadata.duration IS NULL THEN 1.0 ELSE \
session_history_metadata.duration * 1.0 END) * 100) as percent_complete',
'session_history.grandparent_rating_key as grandparent_rating_key',
'session_history.parent_rating_key as parent_rating_key',
'session_history.rating_key as rating_key',
'session_history.user',
'session_history_metadata.media_type',
@@ -112,6 +113,7 @@ class DataFactory(object):
"duration": item["duration"],
"percent_complete": item["percent_complete"],
"grandparent_rating_key": item["grandparent_rating_key"],
"parent_rating_key": item["parent_rating_key"],
"rating_key": item["rating_key"],
"user": item["user"],
"media_type": item["media_type"],
@@ -129,7 +131,7 @@ class DataFactory(object):
return dict
def get_home_stats(self, time_range='30', stat_type='0'):
def get_home_stats(self, time_range='30', stat_type='0', stat_count='5'):
monitor_db = database.MonitorDatabase()
if not time_range.isdigit():
@@ -137,8 +139,11 @@ class DataFactory(object):
sort_type = 'total_plays' if stat_type == '0' else 'total_duration'
if not time_range.isdigit():
stat_count = '5'
# This actually determines the output order in the home page
stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms"]
stats_queries = ["top_tv", "popular_tv", "top_movies", "popular_movies", "top_users", "top_platforms", "last_watched"]
home_stats = []
for stat in stats_queries:
@@ -148,10 +153,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,' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \
'else 0 end) as total_duration, ' \
'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch,' \
'session_history_metadata.grandparent_thumb ' \
@@ -161,7 +166,7 @@ class DataFactory(object):
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "episode" ' \
'GROUP BY session_history_metadata.grandparent_title ' \
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -188,16 +193,58 @@ class DataFactory(object):
'stat_type': sort_type,
'rows': top_tv})
elif 'popular_tv' in stat:
popular_tv = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.grandparent_title, ' \
'COUNT(DISTINCT session_history.user_id) as users_watched, ' \
'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch, ' \
'COUNT(session_history.id) as total_plays, ' \
'session_history_metadata.grandparent_thumb ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "episode" ' \
'GROUP BY session_history_metadata.grandparent_title ' \
'ORDER BY users_watched DESC, total_plays DESC ' \
'LIMIT %s' % (time_range, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
row = {'title': item[1],
'users_watched': item[2],
'rating_key': item[3],
'last_play': item[4],
'total_plays': item[5],
'grandparent_thumb': item[6],
'thumb': '',
'user': '',
'friendly_name': '',
'platform_type': '',
'platform': '',
'row_id': item[0]
}
popular_tv.append(row)
home_stats.append({'stat_id': stat,
'rows': popular_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,' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \
'else 0 end) as total_duration, ' \
'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch,' \
'session_history_metadata.thumb ' \
@@ -207,7 +254,7 @@ class DataFactory(object):
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -234,48 +281,6 @@ class DataFactory(object):
'stat_type': sort_type,
'rows': top_movies})
elif 'popular_tv' in stat:
popular_tv = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.grandparent_title, ' \
'COUNT(DISTINCT session_history.user_id) as users_watched, ' \
'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch, ' \
'COUNT(session_history.id) as total_plays, ' \
'session_history_metadata.grandparent_thumb ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "episode" ' \
'GROUP BY session_history_metadata.grandparent_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': item[6],
'thumb': '',
'user': '',
'friendly_name': '',
'platform_type': '',
'platform': '',
'row_id': item[0]
}
popular_tv.append(row)
home_stats.append({'stat_id': stat,
'rows': popular_tv})
elif 'popular_movies' in stat:
popular_movies = []
try:
@@ -293,7 +298,7 @@ class DataFactory(object):
'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
'LIMIT %s' % (time_range, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -325,10 +330,10 @@ class DataFactory(object):
'(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \
'COUNT(session_history.id) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \
'else 0 end) as total_duration, ' \
'MAX(session_history.started) as last_watch, ' \
'users.custom_avatar_url as thumb, ' \
'users.user_id ' \
@@ -338,7 +343,7 @@ class DataFactory(object):
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") '\
'GROUP BY session_history.user_id ' \
'ORDER BY %s DESC LIMIT 10' % (time_range, sort_type)
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -356,7 +361,7 @@ class DataFactory(object):
'total_plays': item[2],
'total_duration': item[3],
'last_play': item[4],
'thumb': user_thumb,
'user_thumb': user_thumb,
'grandparent_thumb': '',
'users_watched': '',
'rating_key': '',
@@ -377,16 +382,16 @@ 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,' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when session_history.paused_counter is NULL then 0 else session_history.paused_counter end) ' \
'else 0 end) as total_duration, ' \
'MAX(session_history.started) as last_watch ' \
'FROM session_history ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'GROUP BY session_history.platform ' \
'ORDER BY total_plays DESC' % time_range
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -413,6 +418,59 @@ class DataFactory(object):
'stat_type': sort_type,
'rows': top_platform})
elif 'last_watched' in stat:
last_watched = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history.user, ' \
'(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \
'users.user_id, ' \
'users.custom_avatar_url as user_thumb, ' \
'session_history_metadata.full_title, ' \
'session_history_metadata.rating_key, ' \
'session_history_metadata.thumb, ' \
'session_history_metadata.grandparent_thumb, ' \
'MAX(session_history.started) as last_watch, ' \
'session_history.player as platform ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'LEFT OUTER JOIN users ON session_history.user_id = users.user_id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'AND (session_history_metadata.media_type = "movie" ' \
'OR session_history_metadata.media_type = "episode") ' \
'GROUP BY session_history_metadata.full_title ' \
'ORDER BY last_watch DESC ' \
'LIMIT %s' % (time_range, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
if not item[8] or item[8] == '':
thumb = item[7]
else:
thumb = item[8]
row = {'row_id': item[0],
'user': item[1],
'friendly_name': item[2],
'user_id': item[3],
'user_thumb': item[4],
'title': item[5],
'rating_key': item[6],
'thumb': thumb,
'grandparent_thumb': item[8],
'last_watch': item[9],
'platform_type': item[10],
}
last_watched.append(row)
home_stats.append({'stat_id': stat,
'rows': last_watched})
return home_stats
def get_stream_details(self, row_id=None):
@@ -466,24 +524,35 @@ class DataFactory(object):
try:
if user_id:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \
'title, parent_title, 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 ?'
'WHERE user_id = ? ' \
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
' ELSE session_history.rating_key END) ' \
'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, ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \
'title, parent_title, 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 ?'
'WHERE user = ? ' \
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
' ELSE session_history.rating_key END) ' \
'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, ' \
'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"' \
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, session_history.parent_rating_key, ' \
'title, parent_title, 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 ' \
'GROUP BY (CASE WHEN session_history.media_type = "track" THEN session_history.parent_rating_key ' \
' ELSE session_history.rating_key END) ' \
'ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[limit])
except:
@@ -491,24 +560,25 @@ class DataFactory(object):
return None
for row in result:
if row[1] == 'episode' and row[6]:
thumb = row[6]
if row[1] == 'episode' and row[8]:
thumb = row[8]
elif row[1] == 'episode':
thumb = row[7]
thumb = row[9]
else:
thumb = row[5]
thumb = row[7]
recent_output = {'row_id': row[0],
'type': row[1],
'rating_key': row[2],
'title': row[3],
'parent_title': row[4],
'title': row[4],
'parent_title': row[5],
'grandparent_title': row[6],
'thumb': thumb,
'index': row[8],
'parent_index': row[9],
'year': row[10],
'time': row[11],
'user': row[12]
'index': row[10],
'parent_index': row[11],
'year': row[12],
'time': row[13],
'user': row[14]
}
recently_watched.append(recent_output)
@@ -520,8 +590,8 @@ class DataFactory(object):
if row_id:
query = 'SELECT rating_key, parent_rating_key, grandparent_rating_key, title, parent_title, grandparent_title, ' \
'full_title, media_index, parent_media_index, thumb, parent_thumb, grandparent_thumb, art, media_type, ' \
'year, originally_available_at, added_at, updated_at, last_viewed_at, content_rating, summary, rating, ' \
'duration, guid, directors, writers, actors, genres, studio ' \
'year, originally_available_at, added_at, updated_at, last_viewed_at, content_rating, summary, tagline, ' \
'rating, duration, guid, directors, writers, actors, genres, studio ' \
'FROM session_history_metadata ' \
'WHERE id = ?'
result = monitor_db.select(query=query, args=[row_id])
@@ -547,6 +617,7 @@ class DataFactory(object):
'title': item['title'],
'content_rating': item['content_rating'],
'summary': item['summary'],
'tagline': item['tagline'],
'rating': item['rating'],
'duration': item['duration'],
'year': item['year'],

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -33,7 +33,8 @@ class Graphs(object):
if y_axis == 'plays':
query = 'SELECT date(started, "unixepoch", "localtime") as date_played, ' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" then 1 else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'GROUP BY date_played ' \
@@ -42,8 +43,12 @@ class Graphs(object):
result = monitor_db.select(query)
else:
query = 'SELECT date(started, "unixepoch", "localtime") as date_played, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) else 0 end) as movie_duration ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'GROUP BY date_played ' \
@@ -62,31 +67,38 @@ class Graphs(object):
categories = []
series_1 = []
series_2 = []
series_3 = []
for date_item in sorted(date_list):
date_string = date_item.strftime('%Y-%m-%d')
categories.append(date_string)
series_1_value = 0
series_2_value = 0
series_3_value = 0
for item in result:
if date_string == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
break
else:
series_1_value = 0
series_2_value = 0
series_3_value = 0
series_1.append(series_1_value)
series_2.append(series_2_value)
series_3.append(series_3_value)
series_1_output = {'name': 'TV',
'data': series_1}
series_2_output = {'name': 'Movies',
'data': series_2}
series_3_output = {'name': 'Music',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output, series_2_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_per_dayofweek(self, time_range='30', y_axis='plays'):
@@ -105,16 +117,16 @@ class Graphs(object):
'when 4 then "Thursday" ' \
'when 5 then "Friday" ' \
'else "Saturday" end as dayofweek, ' \
'COUNT(id) as total_plays ' \
'from session_history ' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" then 1 else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(media_type = "episode" OR media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'GROUP BY dayofweek ' \
'ORDER BY daynumber'
result = monitor_db.select(query)
y_axis_label = 'Total plays'
else:
query = 'SELECT strftime("%w", datetime(started, "unixepoch", "localtime")) as daynumber, ' \
'case cast (strftime("%w", datetime(started, "unixepoch", "localtime")) as integer) ' \
@@ -125,40 +137,57 @@ class Graphs(object):
'when 4 then "Thursday" ' \
'when 5 then "Friday" ' \
'else "Saturday" end as dayofweek, ' \
'SUM(case when media_type != "track" and stopped > 0 then (stopped - started) else 0 end) as duration ' \
'from session_history ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(media_type = "episode" OR media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'GROUP BY dayofweek ' \
'ORDER BY daynumber'
result = monitor_db.select(query)
y_axis_label = 'Total duration'
days_list = ['Sunday', 'Monday', 'Tuesday', 'Wednesday',
'Thursday', 'Friday', 'Saturday']
categories = []
series_1 = []
series_2 = []
series_3 = []
for day_item in days_list:
categories.append(day_item)
series_1_value = 0
series_2_value = 0
series_3_value = 0
for item in result:
if day_item == item[1]:
series_1_value = item[2]
series_2_value = item[3]
series_3_value = item[4]
break
else:
series_1_value = 0
series_2_value = 0
series_3_value = 0
series_1.append(series_1_value)
series_2.append(series_2_value)
series_3.append(series_3_value)
series_1_output = {'name': y_axis_label,
series_1_output = {'name': 'TV',
'data': series_1}
series_2_output = {'name': 'Movies',
'data': series_2}
series_3_output = {'name': 'Music',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_per_hourofday(self, time_range='30', y_axis='plays'):
@@ -169,28 +198,31 @@ class Graphs(object):
if y_axis == 'plays':
query = 'select strftime("%H", datetime(started, "unixepoch", "localtime")) as hourofday, ' \
'COUNT(id) ' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" then 1 else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(media_type = "episode" OR media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'GROUP BY hourofday ' \
'ORDER BY hourofday'
result = monitor_db.select(query)
y_axis_label = 'Total plays'
else:
query = 'select strftime("%H", datetime(started, "unixepoch", "localtime")) as hourofday, ' \
'SUM(case when media_type != "track" and stopped > 0 then (stopped - started) else 0 end) as duration ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(media_type = "episode" OR media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'GROUP BY hourofday ' \
'ORDER BY hourofday'
result = monitor_db.select(query)
y_axis_label = 'Total duration'
hours_list = ['00','01','02','03','04','05',
'06','07','08','09','10','11',
@@ -199,24 +231,38 @@ class Graphs(object):
categories = []
series_1 = []
series_2 = []
series_3 = []
for hour_item in hours_list:
categories.append(hour_item)
series_1_value = 0
series_2_value = 0
series_3_value = 0
for item in result:
if hour_item == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
break
else:
series_1_value = 0
series_2_value = 0
series_3_value = 0
series_1.append(series_1_value)
series_2.append(series_2_value)
series_3.append(series_3_value)
series_1_output = {'name': y_axis_label,
series_1_output = {'name': 'TV',
'data': series_1}
series_2_output = {'name': 'Movies',
'data': series_2}
series_3_output = {'name': 'Music',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_per_month(self, y_axis='plays'):
@@ -226,7 +272,8 @@ class Graphs(object):
if y_axis == 'plays':
query = 'SELECT strftime("%Y-%m", datetime(started, "unixepoch", "localtime")) as datestring, ' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" then 1 else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-12 months", "localtime") ' \
'GROUP BY strftime("%Y-%m", datetime(started, "unixepoch", "localtime")) ' \
@@ -235,8 +282,12 @@ class Graphs(object):
result = monitor_db.select(query)
else:
query = 'SELECT strftime("%Y-%m", datetime(started, "unixepoch", "localtime")) as datestring, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) else 0 end) as movie_duration ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
'FROM session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-12 months", "localtime") ' \
'GROUP BY strftime("%Y-%m", datetime(started, "unixepoch", "localtime")) ' \
@@ -254,33 +305,40 @@ class Graphs(object):
categories = []
series_1 = []
series_2 = []
series_3 = []
for month_item in sorted(month_range):
dt = datetime.datetime(*month_item[:6])
date_string = dt.strftime('%Y-%m')
categories.append(date_string)
categories.append(dt.strftime('%b %Y'))
series_1_value = 0
series_2_value = 0
series_3_value = 0
for item in result:
if date_string == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
break
else:
series_1_value = 0
series_2_value = 0
series_3_value = 0
series_1.append(series_1_value)
series_2.append(series_2_value)
series_3.append(series_3_value)
series_1_output = {'name': 'TV',
'data': series_1}
series_2_output = {'name': 'Movies',
'data': series_2}
series_3_output = {'name': 'Music',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output, series_2_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_by_top_10_platforms(self, time_range='30', y_axis='plays'):
@@ -291,43 +349,64 @@ class Graphs(object):
if y_axis == 'plays':
query = 'SELECT platform, ' \
'count(id) as platform_count ' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" then 1 else 0 end) as music_count, ' \
'COUNT(id) as total_count ' \
'FROM session_history ' \
'WHERE (datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(media_type = "episode" OR media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime")) ' \
'GROUP BY platform ' \
'ORDER BY platform_count DESC ' \
'ORDER BY total_count DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total plays'
else:
query = 'SELECT platform, ' \
'SUM(case when stopped > 0 then (stopped - started) else 0 end) as duration ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
'WHERE (datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(media_type = "episode" OR media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime")) ' \
'GROUP BY platform ' \
'ORDER BY duration DESC ' \
'ORDER BY total_duration DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total duration'
categories = []
series_1 = []
series_2 = []
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
series_1_output = {'name': y_axis_label,
# Rename Mystery platform names
platform_names = [('Mystery 3', 'Playstation 3'),
('Mystery 4', 'Playstation 4'),
('Mystery 5', 'Xbox 360')]
for old_name, new_name in platform_names:
categories = [item.replace(old_name, new_name) for item in categories]
series_1_output = {'name': 'TV',
'data': series_1}
series_2_output = {'name': 'Movies',
'data': series_2}
series_3_output = {'name': 'Music',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_by_top_10_users(self, time_range='30', y_axis='plays'):
@@ -340,47 +419,61 @@ class Graphs(object):
query = 'SELECT ' \
'(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \
'count(session_history.id) as user_count ' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" then 1 else 0 end) as music_count, ' \
'COUNT(session_history.id) as total_count ' \
'FROM session_history ' \
'JOIN users on session_history.user_id = users.user_id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime")) ' \
'GROUP BY session_history.user_id ' \
'ORDER BY user_count DESC ' \
'ORDER BY total_count DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total plays'
else:
query = 'SELECT ' \
'(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \
'SUM(case when stopped > 0 then (stopped - started) else 0 end) as duration ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
'JOIN users on session_history.user_id = users.user_id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'datetime("now", "-' + time_range + ' days", "localtime")) ' \
'GROUP BY session_history.user_id ' \
'ORDER BY duration DESC ' \
'ORDER BY total_duration DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total duration'
categories = []
series_1 = []
series_2 = []
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
series_1_output = {'name': y_axis_label,
series_1_output = {'name': 'TV',
'data': series_1}
series_2_output = {'name': 'Movies',
'data': series_2}
series_3_output = {'name': 'Music',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_per_stream_type(self, time_range='30', y_axis='plays'):
@@ -392,31 +485,43 @@ class Graphs(object):
try:
if y_axis == 'plays':
query = 'SELECT date(session_history.started, "unixepoch", "localtime") as date_played, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" then 1 else 0 end) as tc_count ' \
'SUM(case when session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play") ' \
'then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy") ' \
'then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode") ' \
'then 1 else 0 end) as tc_count ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
'GROUP BY date_played ' \
'ORDER BY started ASC' % time_range
result = monitor_db.select(query)
else:
query = 'SELECT date(session_history.started, "unixepoch", "localtime") as date_played, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" AND ' \
'session_history.stopped > 0 then (stopped - started) else 0 end) as dp_duration, ' \
'SUM(case when session_history_media_info.video_decision = "copy" AND ' \
'session_history.stopped > 0 then (stopped - started) else 0 end) as ds_duration, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" ' \
'AND session_history.stopped > 0 then (stopped - started) else 0 end) as tc_duration ' \
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
'GROUP BY date_played ' \
'ORDER BY started ASC' % time_range
@@ -465,7 +570,6 @@ class Graphs(object):
output = {'categories': categories,
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_by_source_resolution(self, time_range='30', y_axis='plays'):
@@ -475,49 +579,74 @@ class Graphs(object):
time_range = '30'
if y_axis == 'plays':
query = 'SELECT ' \
'count(session_history.id) as play_count, ' \
'session_history_media_info.video_resolution AS resolution ' \
query = 'SELECT session_history_media_info.video_resolution AS resolution, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play") ' \
'then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy") ' \
'then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode") ' \
'then 1 else 0 end) as tc_count, ' \
'COUNT(session_history.id) as total_count ' \
'FROM session_history ' \
'JOIN session_history_media_info on session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'GROUP BY resolution ' \
'ORDER BY play_count DESC ' \
'ORDER BY total_count DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total plays'
else:
query = 'SELECT ' \
'SUM(case when stopped > 0 then (stopped - started) else 0 end) as duration, ' \
'session_history_media_info.video_resolution AS resolution ' \
query = 'SELECT session_history_media_info.video_resolution AS resolution,' \
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
'JOIN session_history_media_info on session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'GROUP BY resolution ' \
'ORDER BY duration DESC ' \
'ORDER BY total_duration DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total duration'
categories = []
series_1 = []
series_2 = []
series_3 = []
for item in result:
categories.append(item[1])
series_1.append(item[0])
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
series_1_output = {'name': y_axis_label,
series_1_output = {'name': 'Direct Play',
'data': series_1}
series_2_output = {'name': 'Direct Stream',
'data': series_2}
series_3_output = {'name': 'Transcode',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_total_plays_by_stream_resolution(self, time_range='30', y_axis='plays'):
@@ -528,7 +657,6 @@ class Graphs(object):
if y_axis == 'plays':
query = 'SELECT ' \
'count(session_history.id) as play_count, ' \
'(case when session_history_media_info.video_decision = "transcode" then ' \
'(case ' \
'when session_history_media_info.transcode_height <= 360 then "sd" ' \
@@ -538,21 +666,29 @@ class Graphs(object):
'when session_history_media_info.transcode_height <= 1080 then "1080" ' \
'when session_history_media_info.transcode_height <= 1440 then "QHD" ' \
'when session_history_media_info.transcode_height <= 2160 then "4K" ' \
'else "unknown" end) else session_history_media_info.video_resolution end) as resolution ' \
'else "unknown" end) else session_history_media_info.video_resolution end) as resolution, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play") ' \
'then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy") ' \
'then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode") ' \
'then 1 else 0 end) as tc_count, ' \
'COUNT(session_history.id) as total_count ' \
'FROM session_history ' \
'JOIN session_history_media_info on session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'GROUP BY resolution ' \
'ORDER BY play_count DESC ' \
'ORDER BY total_count DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total plays'
else:
query = 'SELECT ' \
'SUM(case when stopped > 0 then (stopped - started) else 0 end) as duration, ' \
'(case when session_history_media_info.video_decision = "transcode" then ' \
'(case ' \
'when session_history_media_info.transcode_height <= 360 then "sd" ' \
@@ -562,31 +698,137 @@ class Graphs(object):
'when session_history_media_info.transcode_height <= 1080 then "1080" ' \
'when session_history_media_info.transcode_height <= 1440 then "QHD" ' \
'when session_history_media_info.transcode_height <= 2160 then "4K" ' \
'else "unknown" end) else session_history_media_info.video_resolution end) as resolution ' \
'else "unknown" end) else session_history_media_info.video_resolution end) as resolution, ' \
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
'JOIN session_history_media_info on session_history.id = session_history_media_info.id ' \
'WHERE (datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime")) AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie") ' \
'GROUP BY resolution ' \
'ORDER BY duration DESC ' \
'ORDER BY total_duration DESC ' \
'LIMIT 10'
result = monitor_db.select(query)
y_axis_label = 'Total duration'
categories = []
series_1 = []
series_2 = []
series_3 = []
for item in result:
categories.append(item[1])
series_1.append(item[0])
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
series_1_output = {'name': y_axis_label,
series_1_output = {'name': 'Direct Play',
'data': series_1}
series_2_output = {'name': 'Direct Stream',
'data': series_2}
series_3_output = {'name': 'Transcode',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output]}
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays'):
monitor_db = database.MonitorDatabase()
if not time_range.isdigit():
time_range = '30'
if y_axis == 'plays':
query = 'SELECT ' \
'session_history.platform as platform, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play") ' \
'then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy") ' \
'then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode") ' \
'then 1 else 0 end) as tc_count, ' \
'COUNT(session_history.id) as total_count ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
'GROUP BY platform ' \
'ORDER BY total_count DESC LIMIT 10'
result = monitor_db.select(query)
else:
query = 'SELECT ' \
'session_history.platform as platform, ' \
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
'GROUP BY platform ' \
'ORDER BY total_duration DESC LIMIT 10'
result = monitor_db.select(query)
categories = []
series_1 = []
series_2 = []
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
# Rename Mystery platform names
platform_names = [('Mystery 3', 'Playstation 3'),
('Mystery 4', 'Playstation 4'),
('Mystery 5', 'Xbox 360')]
for old_name, new_name in platform_names:
categories = [item.replace(old_name, new_name) for item in categories]
series_1_output = {'name': 'Direct Play',
'data': series_1}
series_2_output = {'name': 'Direct Stream',
'data': series_2}
series_3_output = {'name': 'Transcode',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output, series_2_output, series_3_output]}
return output
@@ -599,15 +841,22 @@ class Graphs(object):
if y_axis == 'plays':
query = 'SELECT ' \
'CASE WHEN users.friendly_name is null then users.username else users.friendly_name end as username, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" then 1 else 0 end) as tr_count, ' \
'SUM(case when session_history.media_type != "track" then 1 else 0 end) as total_count ' \
'SUM(case when session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play") ' \
'then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy") ' \
'then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode") ' \
'then 1 else 0 end) as tc_count, ' \
'COUNT(session_history.id) as total_count ' \
'FROM session_history ' \
'JOIN users ON session_history.user_id = users.user_id ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
'GROUP BY username ' \
'ORDER BY total_count DESC LIMIT 10'
@@ -615,85 +864,29 @@ class Graphs(object):
else:
query = 'SELECT ' \
'CASE WHEN users.friendly_name is null then users.username else users.friendly_name end as username, ' \
'SUM(case when session_history.stopped > 0 AND session_history_media_info.video_decision = "direct play" ' \
'then (session_history.stopped - session_history.started) else 0 end) as dp_count, ' \
'SUM(case when session_history.stopped > 0 AND session_history_media_info.video_decision = "copy" ' \
'then (session_history.stopped - session_history.started) else 0 end) as ds_count, ' \
'SUM(case when session_history.stopped > 0 AND session_history_media_info.video_decision = "transcode" ' \
'then (session_history.stopped - session_history.started) else 0 end) as tr_count, ' \
'SUM(case when session_history.stopped > 0 AND session_history.media_type != "track" ' \
'then (session_history.stopped - session_history.started) else 0 end) as total_count ' \
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
'JOIN users ON session_history.user_id = users.user_id ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'datetime("now", "-' + time_range + ' days", "localtime") AND ' \
'(session_history.media_type = "episode" OR session_history.media_type = "movie" OR session_history.media_type = "track") ' \
'GROUP BY username ' \
'ORDER BY total_count DESC LIMIT 10'
result = monitor_db.select(query)
categories = []
series_1 = []
series_2 = []
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
series_1_output = {'name': 'Direct Play',
'data': series_1}
series_2_output = {'name': 'Direct Stream',
'data': series_2}
series_3_output = {'name': 'Transcode',
'data': series_3}
output = {'categories': categories,
'series': [series_1_output, series_2_output, series_3_output]}
return output
def get_stream_type_by_top_10_platforms(self, time_range='30', y_axis='plays'):
monitor_db = database.MonitorDatabase()
if not time_range.isdigit():
time_range = '30'
if y_axis == 'plays':
query = 'SELECT ' \
'session_history.platform as platform, ' \
'SUM(case when session_history_media_info.video_decision = "direct play" then 1 else 0 end) as dp_count, ' \
'SUM(case when session_history_media_info.video_decision = "copy" then 1 else 0 end) as ds_count, ' \
'SUM(case when session_history_media_info.video_decision = "transcode" then 1 else 0 end) as tr_count, ' \
'SUM(case when session_history.media_type != "track" then 1 else 0 end) as total_count ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'GROUP BY platform ' \
'ORDER BY total_count DESC LIMIT 10'
result = monitor_db.select(query)
else:
query = 'SELECT ' \
'session_history.platform as platform, ' \
'SUM(case when session_history.stopped > 0 AND session_history_media_info.video_decision = "direct play" ' \
'then (session_history.stopped - session_history.started) else 0 end) as dp_count, ' \
'SUM(case when session_history.stopped > 0 AND session_history_media_info.video_decision = "copy" ' \
'then (session_history.stopped - session_history.started) else 0 end) as ds_count, ' \
'SUM(case when session_history.stopped > 0 AND session_history_media_info.video_decision = "transcode" ' \
'then (session_history.stopped - session_history.started) else 0 end) as tr_count, ' \
'SUM(case when session_history.stopped > 0 AND session_history.media_type != "track" ' \
'then (session_history.stopped - session_history.started) else 0 end) as total_count ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.started, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
'GROUP BY platform ' \
'ORDER BY total_count DESC LIMIT 10'
'ORDER BY total_duration DESC LIMIT 10'
result = monitor_db.select(query)

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -247,10 +247,10 @@ class MonitorProcessing(object):
else:
stopped = int(time.time())
if plexpy.CONFIG.VIDEO_LOGGING_ENABLE and \
if plexpy.CONFIG.VIDEO_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
(session['media_type'] == 'movie' or session['media_type'] == 'episode'):
logging_enabled = True
elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and \
elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
session['media_type'] == 'track':
logging_enabled = True
else:
@@ -343,17 +343,17 @@ class MonitorProcessing(object):
'grandparent_rating_key, title, parent_title, grandparent_title, full_title, media_index, ' \
'parent_media_index, thumb, parent_thumb, grandparent_thumb, art, media_type, year, ' \
'originally_available_at, added_at, updated_at, last_viewed_at, content_rating, summary, ' \
'rating, duration, guid, directors, writers, actors, genres, studio) VALUES ' \
'tagline, rating, duration, guid, directors, writers, actors, genres, studio) VALUES ' \
'(last_insert_rowid(), ' \
'?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
'?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
args = [session['rating_key'], session['parent_rating_key'], session['grandparent_rating_key'],
session['title'], session['parent_title'], session['grandparent_title'], full_title,
metadata['index'], metadata['parent_index'], metadata['thumb'], metadata['parent_thumb'],
metadata['grandparent_thumb'], metadata['art'], session['media_type'], metadata['year'],
metadata['originally_available_at'], metadata['added_at'], metadata['updated_at'],
metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['rating'],
metadata['duration'], metadata['guid'], directors, writers, actors, genres, metadata['studio']]
metadata['last_viewed_at'], metadata['content_rating'], metadata['summary'], metadata['tagline'],
metadata['rating'], metadata['duration'], metadata['guid'], directors, writers, actors, genres, metadata['studio']]
# logger.debug(u"PlexPy Monitor :: Writing session_history_metadata transaction...")
self.db.action(query=query, args=args)

View File

@@ -162,6 +162,7 @@ def notify(stream_data=None, notify_action=None):
else:
logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.")
def get_notify_state(session):
monitor_db = database.MonitorDatabase()
result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id '
@@ -184,6 +185,7 @@ def get_notify_state(session):
return notify_states
def set_notify_state(session, state, agent_info):
if session and state and agent_info:
@@ -215,6 +217,7 @@ def set_notify_state(session, state, agent_info):
else:
logger.error('PlexPy Notifier :: Unable to set notify state.')
def build_notify_text(session, state):
from plexpy import pmsconnect, helpers
import re
@@ -300,7 +303,13 @@ 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)
stream_duration = 0
if state != 'play':
if session['paused_counter']:
stream_duration = int((time.time() - helpers.cast_to_float(session['started']) -
helpers.cast_to_float(session['paused_counter'])) / 60)
else:
stream_duration = int((time.time() - helpers.cast_to_float(session['started'])) / 60)
progress_percent = helpers.get_percent(view_offset, duration)
@@ -340,14 +349,14 @@ def build_notify_text(session, state):
if on_start_subject and on_start_body:
try:
subject_text = on_start_subject.format(**available_params)
subject_text = unicode(on_start_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = on_start_body.format(**available_params)
body_text = unicode(on_start_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
@@ -364,14 +373,14 @@ def build_notify_text(session, state):
if on_stop_subject and on_stop_body:
try:
subject_text = on_stop_subject.format(**available_params)
subject_text = unicode(on_stop_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = on_stop_body.format(**available_params)
body_text = unicode(on_stop_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
@@ -388,14 +397,14 @@ def build_notify_text(session, state):
if on_pause_subject and on_pause_body:
try:
subject_text = on_pause_subject.format(**available_params)
subject_text = unicode(on_pause_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = on_pause_body.format(**available_params)
body_text = unicode(on_pause_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
@@ -412,14 +421,14 @@ def build_notify_text(session, state):
if on_resume_subject and on_resume_body:
try:
subject_text = on_resume_subject.format(**available_params)
subject_text = unicode(on_resume_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = on_resume_body.format(**available_params)
body_text = unicode(on_resume_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
@@ -436,14 +445,14 @@ def build_notify_text(session, state):
if on_buffer_subject and on_buffer_body:
try:
subject_text = on_buffer_subject.format(**available_params)
subject_text = unicode(on_buffer_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = on_buffer_body.format(**available_params)
body_text = unicode(on_buffer_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
@@ -460,14 +469,14 @@ def build_notify_text(session, state):
if on_watched_subject and on_watched_body:
try:
subject_text = on_watched_subject.format(**available_params)
subject_text = unicode(on_watched_subject).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification subject. Using fallback." % e)
except:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification subject. Using fallback.")
try:
body_text = on_watched_body.format(**available_params)
body_text = unicode(on_watched_body).format(**available_params)
except LookupError, e:
logger.error(u"PlexPy Notifier :: Unable to parse field %s in notification body. Using fallback." % e)
except:
@@ -479,6 +488,7 @@ def build_notify_text(session, state):
else:
return None
def strip_tag(data):
import re

View File

@@ -27,9 +27,8 @@ def refresh_users():
if len(result) > 0:
for item in result:
control_value_dict = {"username": item['username']}
new_value_dict = {"user_id": item['user_id'],
"username": item['username'],
control_value_dict = {"user_id": item['user_id']}
new_value_dict = {"username": item['username'],
"thumb": item['thumb'],
"email": item['email'],
"is_home_user": item['is_home_user'],

View File

@@ -55,6 +55,7 @@ def extract_plexwatch_xml(xml=None):
parent_title = helpers.get_xml_attr(a, 'parentTitle')
studio = helpers.get_xml_attr(a, 'studio')
title = helpers.get_xml_attr(a, 'title')
tagline = helpers.get_xml_attr(a, 'tagline')
directors = []
if a.getElementsByTagName('Director'):
@@ -153,6 +154,7 @@ def extract_plexwatch_xml(xml=None):
'grandparent_title': grandparent_title,
'parent_title': parent_title,
'title': title,
'tagline': tagline,
'guid': guid,
'media_index': media_index,
'originally_available_at': originally_available_at,
@@ -356,6 +358,7 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
'last_viewed_at': extracted_xml['last_viewed_at'],
'content_rating': row['content_rating'],
'summary': row['summary'],
'tagline': extracted_xml['tagline'],
'rating': extracted_xml['rating'],
'duration': extracted_xml['duration'],
'guid': extracted_xml['guid'],

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -90,14 +90,14 @@ class PmsConnect(object):
return request
"""
Return list of episodes in requested season.
Return list of children in requested library item.
Parameters required: rating_key { ratingKey of parent }
Optional parameters: output_format { dict, json }
Output: array
"""
def get_episode_list(self, rating_key='', output_format=''):
def get_children_list(self, rating_key='', output_format=''):
uri = '/library/metadata/' + rating_key + '/children'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
@@ -154,6 +154,38 @@ class PmsConnect(object):
return request
"""
Return list of libraries on server.
Optional parameters: output_format { dict, json }
Output: array
"""
def get_libraries_list(self, output_format=''):
uri = '/library/sections'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
return request
"""
Return list of items in library on server.
Optional parameters: output_format { dict, json }
Output: array
"""
def get_library_list(self, section_key='', list_type='all', count='0', sort_type='', output_format=''):
uri = '/library/sections/' + section_key + '/' + list_type +'?X-Plex-Container-Start=0&X-Plex-Container-Size=' + count + sort_type
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
return request
"""
Return sync item details.
@@ -313,6 +345,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -331,9 +364,44 @@ class PmsConnect(object):
'actors': actors
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'season':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
show_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'index': helpers.get_xml_attr(metadata_main, 'index'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': show_details['metadata']['summary'],
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': show_details['metadata']['duration'],
'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
'art': helpers.get_xml_attr(metadata_main, 'art'),
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': genres,
'actors': actors,
'writers': writers,
'directors': directors
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'episode':
metadata = {'type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
@@ -342,6 +410,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -371,6 +440,7 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
@@ -389,9 +459,7 @@ class PmsConnect(object):
'directors': directors
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'season':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
show_details = self.get_metadata_details(parent_rating_key)
elif metadata_type == 'artist':
metadata = {'type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
@@ -401,9 +469,43 @@ class PmsConnect(object):
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': show_details['metadata']['summary'],
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': show_details['metadata']['duration'],
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
'art': helpers.get_xml_attr(metadata_main, 'art'),
'originally_available_at': helpers.get_xml_attr(metadata_main, 'originallyAvailableAt'),
'added_at': helpers.get_xml_attr(metadata_main, 'addedAt'),
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'writers': writers,
'directors': directors,
'genres': genres,
'actors': actors
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'album':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
artist_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'index': helpers.get_xml_attr(metadata_main, 'index'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': artist_details['metadata']['summary'],
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
@@ -421,8 +523,12 @@ class PmsConnect(object):
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'track':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
album_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(metadata_main, 'grandparentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
@@ -431,9 +537,10 @@ class PmsConnect(object):
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'summary': helpers.get_xml_attr(metadata_main, 'summary'),
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
'duration': helpers.get_xml_attr(metadata_main, 'duration'),
'year': helpers.get_xml_attr(metadata_main, 'year'),
'year': album_details['metadata']['year'],
'thumb': helpers.get_xml_attr(metadata_main, 'thumb'),
'parent_thumb': helpers.get_xml_attr(metadata_main, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(metadata_main, 'grandparentThumb'),
@@ -490,6 +597,12 @@ class PmsConnect(object):
for session in session_data:
session_output = self.get_session_each(session_type, session)
session_list.append(session_output)
if a.getElementsByTagName('Photo'):
session_data = a.getElementsByTagName('Photo')
session_type = 'photo'
for session in session_data:
session_output = self.get_session_each(session_type, session)
session_list.append(session_output)
output = {'stream_count': helpers.get_xml_attr(xml_head[0], 'size'),
'sessions': session_list
@@ -508,8 +621,8 @@ class PmsConnect(object):
def get_session_each(self, stream_type='', session=None):
session_output = None
user_data = users.Users()
if stream_type == 'track':
if stream_type == 'track':
media_info = session.getElementsByTagName('Media')[0]
audio_decision = 'direct play'
audio_channels = helpers.get_xml_attr(media_info, 'audioChannels')
@@ -521,6 +634,9 @@ class PmsConnect(object):
if session.getElementsByTagName('TranscodeSession'):
transcode_session = session.getElementsByTagName('TranscodeSession')[0]
throttled = helpers.get_xml_attr(transcode_session, 'throttled')
transcode_progress = helpers.get_xml_attr(transcode_session, 'progress')
transcode_speed = helpers.get_xml_attr(transcode_session, 'speed')
audio_decision = helpers.get_xml_attr(transcode_session, 'audioDecision')
transcode_audio_channels = helpers.get_xml_attr(transcode_session, 'audioChannels')
transcode_audio_codec = helpers.get_xml_attr(transcode_session, 'audioCodec')
@@ -528,6 +644,9 @@ class PmsConnect(object):
transcode_protocol = helpers.get_xml_attr(transcode_session, 'protocol')
duration = helpers.get_xml_attr(transcode_session, 'duration')
else:
throttled = '0'
transcode_progress = '0'
transcode_speed = ''
transcode_audio_channels = ''
transcode_audio_codec = ''
transcode_container = ''
@@ -552,6 +671,7 @@ class PmsConnect(object):
'user': user_details['username'],
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -562,6 +682,9 @@ class PmsConnect(object):
'rating_key': helpers.get_xml_attr(session, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'),
'throttled': throttled,
'transcode_progress': transcode_progress,
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
'audio_codec': audio_codec,
@@ -587,6 +710,7 @@ class PmsConnect(object):
'type': 'track',
'indexes': 0
}
elif stream_type == 'video':
media_info = session.getElementsByTagName('Media')[0]
audio_decision = 'direct play'
@@ -606,6 +730,9 @@ class PmsConnect(object):
if session.getElementsByTagName('TranscodeSession'):
transcode_session = session.getElementsByTagName('TranscodeSession')[0]
throttled = helpers.get_xml_attr(transcode_session, 'throttled')
transcode_progress = helpers.get_xml_attr(transcode_session, 'progress')
transcode_speed = helpers.get_xml_attr(transcode_session, 'speed')
audio_decision = helpers.get_xml_attr(transcode_session, 'audioDecision')
transcode_audio_channels = helpers.get_xml_attr(transcode_session, 'audioChannels')
transcode_audio_codec = helpers.get_xml_attr(transcode_session, 'audioCodec')
@@ -616,6 +743,9 @@ class PmsConnect(object):
transcode_container = helpers.get_xml_attr(transcode_session, 'container')
transcode_protocol = helpers.get_xml_attr(transcode_session, 'protocol')
else:
throttled = '0'
transcode_progress = '0'
transcode_speed = ''
transcode_audio_channels = ''
transcode_audio_codec = ''
transcode_video_codec = ''
@@ -661,6 +791,7 @@ class PmsConnect(object):
'user': user_details['username'],
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -668,9 +799,13 @@ class PmsConnect(object):
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
'title': helpers.get_xml_attr(session, 'title'),
'year': helpers.get_xml_attr(session, 'year'),
'rating_key': helpers.get_xml_attr(session, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'),
'throttled': throttled,
'transcode_progress': transcode_progress,
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
'audio_codec': audio_codec,
@@ -693,9 +828,13 @@ class PmsConnect(object):
'duration': duration,
'progress': progress,
'progress_percent': str(helpers.get_percent(progress, duration)),
'type': helpers.get_xml_attr(session, 'type'),
'indexes': use_indexes
}
if helpers.get_xml_attr(session, 'ratingKey').isdigit():
session_output['type'] = helpers.get_xml_attr(session, 'type')
else:
session_output['type'] = 'clip'
elif helpers.get_xml_attr(session, 'type') == 'movie':
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
'media_index': helpers.get_xml_attr(session, 'index'),
@@ -708,6 +847,7 @@ class PmsConnect(object):
'user': user_details['username'],
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -715,9 +855,13 @@ class PmsConnect(object):
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
'title': helpers.get_xml_attr(session, 'title'),
'year': helpers.get_xml_attr(session, 'year'),
'rating_key': helpers.get_xml_attr(session, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'),
'throttled': throttled,
'transcode_progress': transcode_progress,
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
'audio_codec': audio_codec,
@@ -740,9 +884,13 @@ class PmsConnect(object):
'duration': duration,
'progress': progress,
'progress_percent': str(helpers.get_percent(progress, duration)),
'type': helpers.get_xml_attr(session, 'type'),
'indexes': use_indexes
}
if helpers.get_xml_attr(session, 'ratingKey').isdigit():
session_output['type'] = helpers.get_xml_attr(session, 'type')
else:
session_output['type'] = 'clip'
elif helpers.get_xml_attr(session, 'type') == 'clip':
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
'media_index': helpers.get_xml_attr(session, 'index'),
@@ -755,6 +903,7 @@ class PmsConnect(object):
'user': user_details['username'],
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -762,9 +911,13 @@ class PmsConnect(object):
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
'title': helpers.get_xml_attr(session, 'title'),
'year': helpers.get_xml_attr(session, 'year'),
'rating_key': helpers.get_xml_attr(session, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'),
'throttled': throttled,
'transcode_progress': transcode_progress,
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
'audio_codec': audio_codec,
@@ -790,49 +943,151 @@ class PmsConnect(object):
'type': helpers.get_xml_attr(session, 'type'),
'indexes': 0
}
elif stream_type == 'photo':
media_info = session.getElementsByTagName('Media')[0]
video_decision = 'direct play'
container = helpers.get_xml_attr(media_info, 'container')
aspect_ratio = helpers.get_xml_attr(media_info, 'aspectRatio')
width = helpers.get_xml_attr(media_info, 'width')
height = helpers.get_xml_attr(media_info, 'height')
if session.getElementsByTagName('TranscodeSession'):
transcode_session = session.getElementsByTagName('TranscodeSession')[0]
throttled = helpers.get_xml_attr(transcode_session, 'throttled')
transcode_progress = helpers.get_xml_attr(transcode_session, 'progress')
transcode_speed = helpers.get_xml_attr(transcode_session, 'speed')
video_decision = helpers.get_xml_attr(transcode_session, 'videoDecision')
transcode_video_codec = helpers.get_xml_attr(transcode_session, 'videoCodec')
transcode_width = helpers.get_xml_attr(transcode_session, 'width')
transcode_height = helpers.get_xml_attr(transcode_session, 'height')
transcode_container = helpers.get_xml_attr(transcode_session, 'container')
transcode_protocol = helpers.get_xml_attr(transcode_session, 'protocol')
else:
throttled = '0'
transcode_progress = '0'
transcode_speed = ''
transcode_video_codec = ''
transcode_width = ''
transcode_height = ''
transcode_container = ''
transcode_protocol = ''
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('_Photo'):
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')[:-6]
else:
machine_id = helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier')
session_output = {'session_key': helpers.get_xml_attr(session, 'sessionKey'),
'media_index': helpers.get_xml_attr(session, 'index'),
'parent_media_index': helpers.get_xml_attr(session, 'parentIndex'),
'art': helpers.get_xml_attr(session, 'art'),
'parent_thumb': helpers.get_xml_attr(session, 'parentThumb'),
'grandparent_thumb': helpers.get_xml_attr(session, 'grandparentThumb'),
'thumb': helpers.get_xml_attr(session, 'thumb'),
'bif_thumb': '',
'user': user_details['username'],
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
'state': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'state'),
'grandparent_title': helpers.get_xml_attr(session, 'grandparentTitle'),
'parent_title': helpers.get_xml_attr(session, 'parentTitle'),
'title': helpers.get_xml_attr(session, 'title'),
'year': helpers.get_xml_attr(session, 'year'),
'rating_key': helpers.get_xml_attr(session, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(session, 'parentRatingKey'),
'grandparent_rating_key': helpers.get_xml_attr(session, 'grandparentRatingKey'),
'throttled': throttled,
'transcode_progress': transcode_progress,
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': '',
'audio_channels': '',
'audio_codec': '',
'video_decision': video_decision,
'video_codec': '',
'height': height,
'width': width,
'container': container,
'bitrate': '',
'video_resolution': '',
'video_framerate': '',
'aspect_ratio': aspect_ratio,
'transcode_audio_channels': '',
'transcode_audio_codec': '',
'transcode_video_codec': transcode_video_codec,
'transcode_width': transcode_width,
'transcode_height': transcode_height,
'transcode_container': transcode_container,
'transcode_protocol': transcode_protocol,
'duration': '',
'progress': '',
'progress_percent': '100',
'type': 'photo',
'indexes': 0
}
else:
logger.warn(u"No known stream types found in session list.")
return session_output
"""
Return processed and validated episode list.
Return processed and validated children list.
Output: array
"""
def get_season_children(self, rating_key=''):
episode_data = self.get_episode_list(rating_key, output_format='xml')
def get_item_children(self, rating_key=''):
children_data = self.get_children_list(rating_key, output_format='xml')
try:
xml_head = episode_data.getElementsByTagName('MediaContainer')
xml_head = children_data.getElementsByTagName('MediaContainer')
except:
logger.warn("Unable to parse XML for get_episode_list.")
logger.warn("Unable to parse XML for get_children_list.")
return []
episode_list = []
children_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
logger.debug(u"No episode data.")
episode_list = {'episode_count': '0',
'episode_list': []
logger.debug(u"No children data.")
children_list = {'children_count': '0',
'children_list': []
}
return episode_list
return parent_list
result_data = []
if a.getElementsByTagName('Directory'):
result_data = a.getElementsByTagName('Directory')
if a.getElementsByTagName('Video'):
result_data = a.getElementsByTagName('Video')
if a.getElementsByTagName('Track'):
result_data = a.getElementsByTagName('Track')
if result_data:
for result in result_data:
episode_output = {'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
children_output = {'rating_key': helpers.get_xml_attr(result, 'ratingKey'),
'index': helpers.get_xml_attr(result, 'index'),
'title': helpers.get_xml_attr(result, 'title'),
'thumb': helpers.get_xml_attr(result, 'thumb')
'thumb': helpers.get_xml_attr(result, 'thumb'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb'),
'duration': helpers.get_xml_attr(result, 'duration')
}
episode_list.append(episode_output)
children_list.append(children_output)
output = {'episode_count': helpers.get_xml_attr(xml_head[0], 'size'),
output = {'children_count': helpers.get_xml_attr(xml_head[0], 'size'),
'children_type': helpers.get_xml_attr(xml_head[0], 'viewGroup'),
'title': helpers.get_xml_attr(xml_head[0], 'title2'),
'episode_list': episode_list
'children_list': children_list
}
return output
@@ -914,6 +1169,150 @@ class PmsConnect(object):
logger.debug(u"Server preferences queried but no parameter received.")
return None
"""
Return processed and validated server libraries list.
Output: array
"""
def get_server_children(self):
libraries_data = self.get_libraries_list(output_format='xml')
try:
xml_head = libraries_data.getElementsByTagName('MediaContainer')
except:
logger.warn("Unable to parse XML for get_libraries_list.")
return []
libraries_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
logger.debug(u"No libraries data.")
libraries_list = {'libraries_count': '0',
'libraries_list': []
}
return libraries_list
if a.getElementsByTagName('Directory'):
result_data = a.getElementsByTagName('Directory')
for result in result_data:
libraries_output = {'key': helpers.get_xml_attr(result, 'key'),
'type': helpers.get_xml_attr(result, 'type'),
'title': helpers.get_xml_attr(result, 'title'),
'thumb': helpers.get_xml_attr(result, 'thumb')
}
libraries_list.append(libraries_output)
output = {'libraries_count': helpers.get_xml_attr(xml_head[0], 'size'),
'title': helpers.get_xml_attr(xml_head[0], 'title1'),
'libraries_list': libraries_list
}
return output
"""
Return processed and validated server library items list.
Parameters required: library_type { movie, show, episode, artist }
section_key { unique library key }
Output: array
"""
def get_library_children(self, library_type='', section_key='', list_type='all', sort_type = ''):
# Currently only grab the library with 1 items so 'size' is not 0
count = '1'
if library_type == 'movie':
sort_type = '&type=1'
elif library_type == 'show':
sort_type = '&type=2'
elif library_type == 'episode':
sort_type = '&type=4'
elif library_type == 'album':
list_type = 'albums'
library_data = self.get_library_list(section_key, list_type, count, sort_type, output_format='xml')
try:
xml_head = library_data.getElementsByTagName('MediaContainer')
except:
logger.warn("Unable to parse XML for get_library_children.")
return []
library_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
logger.debug(u"No library data.")
library_list = {'library_count': '0',
'library_list': []
}
return library_list
if a.getElementsByTagName('Directory'):
result_data = a.getElementsByTagName('Directory')
for result in result_data:
library_output = {'key': helpers.get_xml_attr(result, 'key'),
'type': helpers.get_xml_attr(result, 'type'),
'title': helpers.get_xml_attr(result, 'title'),
'thumb': helpers.get_xml_attr(result, 'thumb')
}
library_list.append(library_output)
output = {'library_count': helpers.get_xml_attr(xml_head[0], 'totalSize'),
'count_type': helpers.get_xml_attr(xml_head[0], 'title2'),
'library_list': library_list
}
return output
"""
Return processed and validated server statistics.
Output: array
"""
def get_library_stats(self):
server_libraries = self.get_server_children()
server_library_stats = []
if server_libraries['libraries_count'] != '0':
libraries_list = server_libraries['libraries_list']
for library in libraries_list:
library_type = library['type']
section_key = library['key']
library_list = self.get_library_children(library_type, section_key)
if library_list['library_count'] != '0':
library_stats = {'title': library['title'],
'thumb': library['thumb'],
'count': library_list['library_count'],
'count_type': library_list['count_type']
}
if library_type == 'show':
episode_list = self.get_library_children(library_type='episode', section_key=section_key)
episode_stats = {'episode_count': episode_list['library_count'],
'episode_count_type': 'All Episodes'
}
library_stats.update(episode_stats)
if library_type == 'artist':
album_list = self.get_library_children(library_type='album', section_key=section_key)
album_stats = {'album_count': album_list['library_count'],
'album_count_type': 'All Albums'
}
library_stats.update(album_stats)
server_library_stats.append({'type': library_type,
'rows': library_stats})
return server_library_stats
"""
Return image data as array.
Array contains the image content type and image binary

View File

@@ -40,7 +40,9 @@ class Users(object):
'session_history_metadata.media_type',
'session_history.rating_key as rating_key',
'session_history_media_info.video_decision',
'users.username as user'
'users.username as user',
'users.do_notify as do_notify',
'users.keep_history as keep_history'
]
try:
query = data_tables.ssp_query(table_name='users',
@@ -94,7 +96,9 @@ class Users(object):
"video_decision": item['video_decision'],
"user_thumb": user_thumb,
"user": item["user"],
"user_id": item['user_id']
"user_id": item['user_id'],
"do_notify": helpers.checked(item['do_notify']),
"keep_history": helpers.checked(item['keep_history'])
}
rows.append(row)

View File

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

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -66,7 +66,9 @@ class WebInterface(object):
def home(self):
config = {
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE
"home_stats_type": plexpy.CONFIG.HOME_STATS_TYPE,
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
}
return serve_template(templatename="index.html", title="Home", config=config)
@@ -119,12 +121,19 @@ class WebInterface(object):
return json.dumps(formats)
@cherrypy.expose
def home_stats(self, time_range='30', stat_type='0', **kwargs):
def home_stats(self, time_range='30', stat_type='0', stat_count='5', **kwargs):
data_factory = datafactory.DataFactory()
stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type)
stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type, stat_count=stat_count)
return serve_template(templatename="home_stats.html", title="Stats", data=stats_data)
@cherrypy.expose
def library_stats(self, **kwargs):
pms_connect = pmsconnect.PmsConnect()
stats_data = pms_connect.get_library_stats()
return serve_template(templatename="library_stats.html", title="Library Stats", data=stats_data)
@cherrypy.expose
def history(self):
return serve_template(templatename="history.html", title="History")
@@ -453,6 +462,7 @@ class WebInterface(object):
"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),
"home_stats_count": plexpy.CONFIG.HOME_STATS_COUNT,
"buffer_threshold": plexpy.CONFIG.BUFFER_THRESHOLD,
"buffer_wait": plexpy.CONFIG.BUFFER_WAIT
}
@@ -492,17 +502,33 @@ class WebInterface(object):
kwargs[plain_config] = kwargs[use_config]
del kwargs[use_config]
# Check if we should refresh our data
refresh_users = False
reschedule = False
if 'monitoring_interval' in kwargs and 'refresh_users_interval' in kwargs:
if (kwargs['monitoring_interval'] != str(plexpy.CONFIG.MONITORING_INTERVAL)) or \
(kwargs['refresh_users_interval'] != str(plexpy.CONFIG.REFRESH_USERS_INTERVAL)):
reschedule = True
if 'pms_ip' in kwargs:
if kwargs['pms_ip'] != plexpy.CONFIG.PMS_IP:
refresh_users = True
plexpy.CONFIG.process_kwargs(kwargs)
# Write the config
plexpy.CONFIG.write()
# Reconfigure scheduler
plexpy.initialize_scheduler()
# Get new server URLs for SSL communications.
plextv.get_real_pms_url()
# Refresh users table. Probably shouldn't do this on every config save, will improve this later.
# Reconfigure scheduler if intervals changed
if reschedule:
plexpy.initialize_scheduler()
# Refresh users table if our server IP changes.
if refresh_users:
threading.Thread(target=plextv.refresh_users).start()
raise cherrypy.HTTPRedirect("settings")
@@ -551,6 +577,9 @@ class WebInterface(object):
if 'rating_key' in kwargs:
rating_key = kwargs.get('rating_key', "")
custom_where = [['rating_key', rating_key]]
if 'parent_rating_key' in kwargs:
rating_key = kwargs.get('parent_rating_key', "")
custom_where = [['parent_rating_key', rating_key]]
if 'grandparent_rating_key' in kwargs:
rating_key = kwargs.get('grandparent_rating_key', "")
custom_where = [['grandparent_rating_key', rating_key]]
@@ -731,16 +760,23 @@ class WebInterface(object):
@cherrypy.expose
def info(self, item_id=None, source=None, **kwargs):
metadata = None
config = {
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER
}
if source == 'history':
data_factory = datafactory.DataFactory()
result = data_factory.get_metadata_details(row_id=item_id)
metadata = data_factory.get_metadata_details(row_id=item_id)
else:
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_metadata_details(rating_key=item_id)['metadata']
result = pms_connect.get_metadata_details(rating_key=item_id)
if result:
return serve_template(templatename="info.html", data=result, title="Info")
metadata = result['metadata']
if metadata:
return serve_template(templatename="info.html", data=metadata, title="Info", config=config)
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="info.html", data=None, title="Info")
@@ -785,16 +821,16 @@ class WebInterface(object):
return serve_template(templatename="user_platform_stats.html", data=None, title="Platform Stats")
@cherrypy.expose
def get_children(self, rating_key='', **kwargs):
def get_item_children(self, rating_key='', **kwargs):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_season_children(rating_key)
result = pms_connect.get_item_children(rating_key)
if result:
return serve_template(templatename="info_episode_list.html", data=result, title="Episode List")
return serve_template(templatename="info_children_list.html", data=result, title="Children List")
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="info_episode_list.html", data=None, title="Episode List")
return serve_template(templatename="info_children_list.html", data=None, title="Children List")
@cherrypy.expose
def get_metadata_json(self, rating_key='', **kwargs):