Compare commits

...

206 Commits
v1.0 ... v1.1.7

Author SHA1 Message Date
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
drzoidberg33
0f04a6d25b Merge pull request #110 from drzoidberg33/revert-108-info-pages
Revert "Info pages"
2015-08-23 00:09:29 +02:00
drzoidberg33
2d19accdd1 Revert "Info pages" 2015-08-23 00:08:55 +02:00
drzoidberg33
3d5706002d Merge pull request #108 from JonnyWong16/info-pages
Info pages
2015-08-23 00:08:50 +02:00
Tim
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
Tim
abec036cb2 v1.1.3 2015-08-22 17:11:04 +02:00
drzoidberg33
8bdd40f011 Merge pull request #105 from JonnyWong16/history-tooltips
History tooltips
2015-08-22 16:32:52 +02:00
drzoidberg33
b98c17e738 Merge pull request #104 from JonnyWong16/stream_duration-notification
Stream duration notification option
2015-08-22 16:32:35 +02:00
Jonathan Wong
a71c6e6592 Fix Air date and runtime 2015-08-22 05:24:06 -07:00
Jonathan Wong
545b7cb581 Remove grandparent_rating_key from season media type 2015-08-22 05:24:02 -07:00
Jonathan Wong
0848e317ee Add links to info pages to move between show / season / episode
* Clicking poster moves up one level
* Links in titles of season and episodes
2015-08-22 05:18:33 -07:00
Jonathan Wong
e976e6cf5c Added more detail to history items
* Year for movies
* Season and episode number for episodes
* Album name for tracks
2015-08-22 05:17:30 -07:00
Jonathan Wong
95cd2b4f81 Forgot semicolon 2015-08-22 03:13:23 -07:00
Jonathan Wong
3df73dc287 Implement changes to history table to user tables
* Add tooltips
* Change icons
2015-08-22 01:20:57 -07:00
Jonathan Wong
3a703eb605 Move icons to the left
* Added icons for "direct play" and "direct stream"
* Separate icons for "movie" and "episode"
2015-08-22 01:20:11 -07:00
Jonathan Wong
e94c00ca48 Fix history table on info pages 2015-08-22 01:17:58 -07:00
Jonathan Wong
d6a34b3e6b Get thumbnails for tooltips in history table 2015-08-21 03:56:47 -07:00
Jonathan Wong
3cbf05d54a Update settings page to show available stream_duration 2015-08-21 03:07:18 -07:00
Jonathan Wong
cba8608c23 Adds stream_duration to notification options 2015-08-21 02:59:49 -07:00
Jonathan Wong
e34865d0dd Initial implementation of tooltips in history table
* Move transcode icon to platform column
* Tooltips for transcode icon and media type icon
* Popover for album art when hover over title
2015-08-20 21:55:11 -07:00
drzoidberg33
b2a7f639bb Merge pull request #100 from JonnyWong16/dev
Some more changes
2015-08-20 22:10:11 +02:00
JonnyWong16
fcbc921470 Fix delete history row only if the button is clicked
History row was deleting even when clicking in the table cell outside
the button.
2015-08-20 12:13:54 -07:00
JonnyWong16
5168d76e86 Add "Last Platform" and "Last Watched" to user data tables 2015-08-20 12:13:52 -07:00
JonnyWong16
968d213b97 Fix platform text clickable and swap column order
Other minor changes to history tables:
* hidden columns with  smaller screen size
* font colour
2015-08-20 12:13:51 -07:00
JonnyWong16
9fc4573c42 Show 0 mins with 0 plays on user page. 2015-08-20 12:13:49 -07:00
drzoidberg33
9adf5cc39a Merge pull request #96 from JonnyWong16/dev
More stylized homepage statistics and other style changes
2015-08-19 23:29:58 +02:00
JonnyWong16
2ca04f4a8b Missed a file in last commit 2015-08-19 13:54:11 -07:00
JonnyWong16
fd3b2a48f9 Revert home stats poster size. 2015-08-19 13:53:23 -07:00
JonnyWong16
01b3ae377b Add clear search button to data tables. 2015-08-19 13:25:29 -07:00
JonnyWong16
5a1516286c Changes to info pages
* Style changes
* Added missing metadata
* Don't show metadata field if data is unavailable
2015-08-19 01:13:55 -07:00
JonnyWong16
317a9f0b8e Dynamically adjust user recently watched items
Only show one row of recently watched items which adjusts to the window
size, similar to recently added items.
2015-08-19 00:51:27 -07:00
JonnyWong16
c98505038a Fallback to show poster if season poster is unavailable. 2015-08-19 00:50:19 -07:00
JonnyWong16
1ec1edefdd More style changes across site 2015-08-18 19:05:14 -07:00
JonnyWong16
aa351bd965 Fixed graphs days text not updating with cookies. 2015-08-18 17:05:34 -07:00
JonnyWong16
519ff6b203 Fixed default Gravatar on homepage. 2015-08-18 16:47:49 -07:00
JonnyWong16
5b2beb2b81 More stylized homepage statistics
Also fixed some formatting on user page and posters on info page.
2015-08-18 16:44:17 -07:00
Tim
13e6a70a30 Fix typo on user edit page.
Change styling of version number and changelog button.
2015-08-19 00:03:49 +02:00
Tim
7e99eb7a2a Update README. 2015-08-18 23:39:20 +02:00
Tim
6efaabb630 Remove some more Headphones references. 2015-08-18 23:27:09 +02:00
Tim
2536fdf17b Fix month display showing "invalid date" on totals graph.
Make duration on home stats human readable.
2015-08-18 22:59:24 +02:00
drzoidberg33
7e8a427107 Merge pull request #95 from JonnyWong16/dev
Update to homepage statistics
2015-08-18 22:50:29 +02:00
JonnyWong16
bbaf428fd8 Fix "Select columns" button text 2015-08-18 13:41:34 -07:00
JonnyWong16
7dfd063138 Update documentation for home stats 2015-08-18 12:36:10 -07:00
JonnyWong16
5c94b21bd1 Update to homepage statistics
* Added most popular movie to homepage
* New setting to toggle statistics based on play duration instead of
play count
2015-08-18 12:32:41 -07:00
drzoidberg33
58474d9565 Merge pull request #87 from JonnyWong16/dev
Use cookies to save graph state.
2015-08-18 13:36:30 +02:00
JonnyWong16
6cb1c057cf Another update to recently added and recently watched.
Recently added albums now include artist name.
Recently watched episodes now include show name.
2015-08-18 02:26:52 -07:00
JonnyWong16
22cc06dec3 Fixed tab spacing 2015-08-17 16:25:27 -07:00
JonnyWong16
357797df6b Use cookies to save graph state.
The selected graph state is remembered on refreshing the graph page.
2015-08-17 16:23:45 -07:00
Tim
4c6f6ca736 Clearer version info in Settings menu.
Rudimentary changelog reader (Settings menu -> General)
Reverse the changelog order - newer changes first.
2015-08-18 01:05:12 +02:00
drzoidberg33
c0214f1489 Merge pull request #83 from JonnyWong16/dev
Fixed recently added and recently watched posters.
2015-08-17 22:38:31 +02:00
JonnyWong16
dd27f9bf72 Fixed recently added and recently watched posters.
Changed posters to match Plex/Web style, and fixed stretching on "home
video" posters.
2015-08-17 13:03:44 -07:00
Tim
c1c7911d08 Fix IP modal on history tab of user page. 2015-08-17 21:21:02 +02:00
drzoidberg33
755e9107fa Merge pull request #79 from JonnyWong16/dev
Two digit season and episode numbers and FreeNAS init script
2015-08-17 11:19:42 +02:00
JonnyWong16
3bb6320fc1 Modified FreeBSD init script for FreeNAS 2015-08-16 23:25:14 -07:00
JonnyWong16
b5ad88ae5a Two digit season and episode numbers for notifications. 2015-08-16 23:24:52 -07:00
Tim
bbcf3bf7da Don't reset to page 1 on history row delete. 2015-08-16 23:45:17 +02:00
Tim
51e1949538 Hide delete and watched columns in column selector.
Add delete mode to user history tab too.
Some styling changes.
2015-08-16 23:30:45 +02:00
Tim
6b1a57e650 Add "delete mode" on history table allows individual rows to be deleted permanently.
Add user history purge option in edit user screen. Will remove all history for selected user.
2015-08-16 22:52:08 +02:00
Tim
8e57df53fd Report the correct version numbers. 2015-08-16 13:48:39 +02:00
Tim
cea4992331 v1.1.2 2015-08-16 13:02:38 +02:00
Tim
c98a8865d6 Fix bug where user refresh would fail under certain circumstances.
Move all user functions to it's own class.
2015-08-16 12:40:32 +02:00
Tim
fd3daae491 v1.1.1 2015-08-15 21:54:43 +02:00
drzoidberg33
697d107952 Merge pull request #71 from jroyal/include-show-title-in-rec-add
Recently added TV Shows display title of the show
2015-08-15 21:49:29 +02:00
drzoidberg33
65e42be278 Merge pull request #69 from jroyal/feature-show-most-watched-movie
Most watched movie is now on home page
2015-08-15 21:48:27 +02:00
Tim
ad79d860db Allow the buffer warnings to be completely disabled by setting buffer threshold to 0.
Fix bug with buffer warnings where notification would trigger continuously after first trigger.
Fix bug where custom avatar URL would get reset on every user refresh.
2015-08-15 21:30:31 +02:00
James Royal
5826a823a8 Add parent_title to the docs of recently_added.html 2015-08-15 13:52:50 -05:00
James Royal
cbec1e7768 Recently added TV Shows display title of the show 2015-08-15 13:28:32 -05:00
James Royal
40f72bbe5f Most watched movie is now on home page 2015-08-15 12:26:37 -05:00
Tim
699f481308 v1.1.0 2015-08-15 18:20:53 +02:00
Tim
411c28a10b Fix bug when updating user profile pic without user_id. 2015-08-15 17:49:54 +02:00
drzoidberg33
375bd733f1 Merge pull request #66 from jroyal/feature-allow-custom-profile-pic
Feature allow custom profile pic
2015-08-15 15:59:02 +02:00
Tim
a96482ee3c New pause, resume and buffer notification options.
Reworked notification config in settings menu.
2015-08-15 14:37:27 +02:00
James Royal
5fa6489733 Remove unneeded print statement 2015-08-14 23:56:29 -05:00
James Royal
804a667b19 New profile URLs are saved to the database 2015-08-14 23:52:28 -05:00
James Royal
7a7c92191d Add field to set new profile pic url. Support retrieving only. 2015-08-14 23:33:16 -05:00
Tim
b7baf1a05d Fix alignment of weekend bands on graphs. 2015-08-14 21:38:51 +02:00
Tim
c4416572cf Remove unnecessary transparency on update notification and column selection.
Keep the update message closed for 1 hour if close button is clicked. Will reset if manual update check is done.
Don't ignore user logging when doing import (we still need an option in the import to override this).
2015-08-14 20:15:30 +02:00
drzoidberg33
5062c6e67a Merge pull request #63 from jroyal/dev
Allow a user to change home page statistics day length
2015-08-14 19:37:49 +02:00
James Royal
3fc2f43b79 Create an option in settings to change home statistics length 2015-08-13 22:10:24 -05:00
James Royal
a0bd94397c Add 7 days button to graph page 2015-08-13 22:08:25 -05:00
Tim
ad12a85c6c Show all users in Users table even if they don't have any history. 2015-08-14 01:05:35 +02:00
Tim
e8e5a0b5ff Fix home stats not displaying even if non-TV items have been logged. 2015-08-14 00:20:30 +02:00
Tim
0877a6bf21 Add per-user history logging toggle. Uncheck to disable history logging for specific user.
On SSL cert bypass check that the method is available before allowing.
2015-08-13 23:05:09 +02:00
drzoidberg33
b0ded77571 Merge pull request #60 from drzoidberg33/master
v1.0.1
2015-08-13 19:25:59 +02:00
Tim
001acfc24d v1.0.1 2015-08-13 19:20:23 +02:00
Tim
5abe314f9f Fix typo on graphs page causing date selection to break on Safari.
Add back bootstrap.css.map
2015-08-13 19:05:29 +02:00
Tim
d345893529 Allow SSL cert verify override on Plex.tv requests. 2015-08-12 23:18:37 +02:00
Tim
2c77f2df98 Update contrib file. 2015-08-11 22:56:16 +02:00
58 changed files with 6344 additions and 2197 deletions

View File

@@ -1,5 +1,100 @@
# Changelog
## v1.0 (2015-07-11)
## 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 @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.
## v1.1.2 (2015-08-16)
* Fix bug where user refresh would fail under certain circumstances.
## v1.1.1 (2015-08-15)
* Added Most watched movie for home stats. Thanks @jroyal.
* Added TV show title to recently added text. Thanks @jroyal.
* Fix bug with buffer warnings where notification would trigger continuously after first trigger.
* Fix bug where custom avatar URL would get reset on every user refresh.
## v1.1.0 (2015-08-15)
* Add option to disable all history logging per user.
* Add option to change user avatar URL. Thanks @jroyal.
* Show all users on users table even if they don't yet have history.
* Add option to change time frame of statistics on home page (Settings -> Extra Settings). Thanks @jroyal.
* Add 7 day period for graphs. Thanks @jroyal.
* Add pause, resume and buffer warning notification options.
* Add fine tuning settings for buffer warning triggers.
* Fix issue with SSL cert verification bypass when method doesn't exist (depends on Python version).
* Fix bug on home stats which wouldn't update unless a TV show was first logged.
* Fix alignment of bands on daily graphs which highlight weekends.
* Fix behaviour of close button on update popup, will now stay closed for an hour after clicking close.
* Fix some styling niggles.
## v1.0.1 (2015-08-13)
* Allow SSL certificate check override for certain systems with bad CA stores.
* Fix typo on graphs page causing date selection to break on Safari.
## v1.0 (2015-08-11)
* First release

View File

@@ -12,7 +12,7 @@ In case you read this because you are posting an issue, please take a minute and
If you think you can contribute code to the PlexPy repository, do not hesitate to submit a pull request.
### Branches
All pull requests should be based on the `develop` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/develop -b FEATURE_NAME`. Use meaningful commit messages.
All pull requests should be based on the `dev` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/dev -b FEATURE_NAME`. Use meaningful commit messages.
### Python Code

View File

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

View File

@@ -1,4 +1,4 @@
<%
<%
import plexpy
from plexpy import version
%>
@@ -30,16 +30,16 @@ from plexpy import version
<div class="container">
<div id="ajaxMsg" class="ajaxMsg"></div>
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
<div id="updatebar">
<div id="updatebar" style="display: none;">
You're running an unknown version of PlexPy. <a href="update">Update</a> or
<a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
<a href="#" id="updateDismiss">Close</a>
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
<div id="updatebar">
<div id="updatebar" style="display: none;">
A <a
href="https://github.com/${plexpy.CONFIG.GIT_USER}/plexpy/compare/${plexpy.CURRENT_VERSION}...${plexpy.LATEST_VERSION}">
newer version</a> is available. You're ${plexpy.COMMITS_BEHIND} commits behind. <a href="update">Update</a> or
<a href="#" onclick="$('#updatebar').slideUp('slow');">Close</a>
<a href="#" id="updateDismiss">Close</a>
</div>
% endif
<nav class="navbar navbar-fixed-top">
@@ -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">
@@ -99,11 +99,24 @@ from plexpy import version
</div>
${next.headerIncludes()}
${next.body()}
<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>
<script src="interfaces/default/js/script.js"></script>
<script>
$('#updateDismiss').click(function() {
$('#updatebar').slideUp('slow');
// Set cookie to remember dismiss decision for 1 hour.
setCookie('updateDismiss', 'true', 1/24);
});
if (!getCookie('updateDismiss')) {
$('#updatebar').show();
}
</script>
${next.javascriptIncludes()}
</body>
</html>

File diff suppressed because one or more lines are too long

View File

@@ -72,9 +72,10 @@ ul.ColVis_collection {
width: 150px;
padding: 8px 8px 4px 8px;
margin: 10px 0px 0px 0px;
background-color: rgba( 88, 88, 88, 0.8 );
background-color: #444;
overflow: hidden;
z-index: 2002;
border-radius: 4px;
}
ul.ColVis_collection li {

File diff suppressed because it is too large Load Diff

View File

@@ -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,159 +66,183 @@ 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':
<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']}"/>
% 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['type'] == 'track':
<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:
${a['title']}
% 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:
<div class="dashboard-activity-poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=500&height=280);"></div>
% endif
% endif
% 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
% endif
<br>
</div>
</div>
<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>
</div>
<div class='dashboard-activity-metadata-wrapper'>
<div class='dashboard-activity-instance-overlay'>
<div class='dashboard-activity-metadata-progress-minutes'>
<div class='activity-progress'>
<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-title">
% if a['type'] == 'episode':
<a href="info?item_id=${a['rating_key']}">${a['grandparent_title']} - ${a['title']}</a>
% elif a['type'] == 'movie':
<a href="info?item_id=${a['rating_key']}">${a['title']}</a>
% elif a['type'] == 'clip':
${a['title']}
% elif a['type'] == 'track':
${a['grandparent_title']} - ${a['title']}
% else:
${a['grandparent_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>
% else:
Stream: <strong>Transcoding</strong>
% 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>
% else:
Stream: <strong>Transcoding</strong>
% 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>
% if a['type'] == 'movie' or a['type'] == 'episode':
</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-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 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':
<a href="info?item_id=${a['rating_key']}">${a['title']}</a>
% elif a['type'] == 'clip':
${a['title']}
% elif a['type'] == 'track':
${a['grandparent_title']} - ${a['title']}
% else:
${a['grandparent_title']} - ${a['title']}
% endif
</div>
<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['parent_title']}
% else:
${a['year']}
% endif
</div>
<div class="dashboard-activity-metadata-user">
% if a['user_id']:
<a href="user?user_id=${a['user_id']}">${a['friendly_name']}</a>
% else:
<a href="user?user=${a['user']}">${a['friendly_name']}</a>
% endif
</div>
</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).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:
@@ -223,4 +252,4 @@ DOCUMENTATION :: END
<div class="text-muted">There was an error communicating with your Plex Server. Please check your <a
href="settings">settings</a>.
</div><br>
% endif
% endif

View File

@@ -14,6 +14,7 @@ user Return the real Plex username
user_id Return the Plex user_id
friendly_name Returns the friendly edited Plex username
do_notify Returns bool value for whether the user should trigger notifications
keep_history Returns bool value for whether the user's activity should be logged
DOCUMENTATION :: END
</%doc>
@@ -30,18 +31,39 @@ DOCUMENTATION :: END
<div class="form-group">
<label for="friendly_name">Friendly Name</label>
<div class="row">
<div class="col-xs-6">
<div class="col-md-6">
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${data['friendly_name']}" size="30">
</div>
</div>
<p class="help-block">Replace all occurances of the username with this name.</p>
</div>
<div class="form-group">
<label for="profile_url">Profile Picture URL</label>
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" id="profile_url" name="profile_url" value="${data['thumb']}">
</div>
</div>
<p class="help-block">Change the users profile picture in PlexPy. To reset to default, leave this field empty and save then perform a user refresh.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${data['do_notify']}> Enable notifications
</label>
<p class="help-block">Uncheck this if you do not want to receive notifications for this user's activity.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${data['keep_history']}> Keep history
</label>
<p class="help-block">Uncheck this if you do not want this keep any history on this user's activity.</p>
</div>
% if data['user_id']:
<div class="form-group">
<button class="btn btn-danger" id="delete-all-history">Purge</button>
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
</div>
% endif
</fieldset>
</div>
<div class="modal-footer">
@@ -52,19 +74,42 @@ 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() {
var friendly_name = $("#friendly_name").val();
var thumb = $("#profile_url").val();
var do_notify = 0;
var keep_history = 0;
if ($("#do_notify").is(":checked")) {
do_notify = 1;
}
if ($("#keep_history").is(":checked")) {
keep_history = 1;
}
% if data['user_id']:
$.ajax({
url: 'edit_user',
data: {user_id: '${data['user_id']}', friendly_name: friendly_name, do_notify: do_notify},
data: {user_id: '${data['user_id']}', friendly_name: friendly_name, do_notify: do_notify, keep_history: keep_history, thumb: thumb},
cache: false,
async: true,
success: function(data) {
@@ -72,12 +117,13 @@ DOCUMENTATION :: END
if ($.trim(friendly_name) !== '') {
$(".set-username").html(friendly_name);
}
$("#user-profile-thumb").attr('src', thumb);
}
});
% else:
$.ajax({
url: 'edit_user',
data: {user: '${data['user']}', friendly_name: friendly_name, do_notify: do_notify},
data: {user: '${data['user']}', friendly_name: friendly_name, do_notify: do_notify, keep_history: keep_history, thumb: thumb},
cache: false,
async: true,
success: function(data) {
@@ -85,10 +131,53 @@ DOCUMENTATION :: END
if ($.trim(friendly_name) !== '') {
$(".set-username").html(friendly_name);
}
$("#user-profile-thumb").attr('src', thumb);
}
});
% endif
});
$("#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']}'},
cache: false,
async: true,
success: function(data) {
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>
% endif

View File

@@ -21,6 +21,9 @@
</label>
</div>
<div class="btn-group" data-toggle="buttons" id="days-selection">
<label class="btn btn-dark">
<input type="radio" name="date-options" id="graph-7" value="7" autocomplete="off"> 7 days
</label>
<label class="btn btn-dark active">
<input type="radio" name="date-options" id="graph-30" value="30" autocomplete="off" checked> 30 days
</label>
@@ -260,9 +263,36 @@
<script>
$(document).ready(function () {
var current_range = 30;
// Save graph state to cookies
$('input[name=yaxis-options]').change(function() {
setCookie('graphType', $(this).val(), 365, '/');
});
$('input[name=date-options]').change(function() {
setCookie('graphDate', $(this).val(), 365, '/');
});
$('a[data-toggle=tab]').click(function() {
setCookie('graphTab', $(this).attr('href'), 365, '/');
});
// Initial values for graph if no saved state
var yaxis = 'plays';
var current_range = 30;
var current_tab = '#tabs-1';
// Read saved graph state from cookies and set initial values
if(getCookie('graphType')) {
var yaxis = getCookie('graphType');
$('input[name=yaxis-options][value=' + yaxis + ']').prop('checked', true).trigger('click');
}
if(getCookie('graphDate')) {
var current_range = getCookie('graphDate');
$('input[name=date-options][value=' + current_range + ']').prop('checked', true).trigger('click');
$('.days').html(current_range);
}
if(getCookie('graphTab')) {
var current_tab = getCookie('graphTab');
$('a[data-toggle=tab][href=' + current_tab + ']').trigger('click');
}
function loadGraphsTab1(time_range, yaxis) {
setGraphFormat(yaxis);
@@ -280,8 +310,8 @@
if ((moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sat') ||
(moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sun')) {
hc_plays_by_day_options.xAxis.plotBands.push({
from: i,
to: i+1,
from: i-0.5,
to: i+0.5,
color: 'rgba(80,80,80,0.3)'
});
}
@@ -358,8 +388,8 @@
if ((moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sat') ||
(moment(data.categories[i], 'YYYY-MM-DD').format('ddd') == 'Sun')) {
hc_plays_by_stream_type_options.xAxis.plotBands.push({
from: i,
to: i+1,
from: i-0.5,
to: i+0.5,
color: 'rgba(80,80,80,0.3)'
});
}
@@ -429,12 +459,8 @@
data: { y_axis: yaxis },
dataType: "json",
success: function(data) {
var dateArray = [];
for (var i = 0; i < data.categories.length; i++) {
dateArray.push(moment(data.categories[i], 'YYYY-MM').format('MMM YYYY'));
}
hc_plays_by_month_options.yAxis.min = 0;
hc_plays_by_month_options.xAxis.categories = dateArray;
hc_plays_by_month_options.xAxis.categories = data.categories;
hc_plays_by_month_options.series = data.series;
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
}
@@ -461,7 +487,7 @@
})
// Tab3 opened
$('#graph-tabs a[href="#tabs-3"').on('shown.bs.tab', function (e) {
$('#graph-tabs a[href="#tabs-3"]').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
$('#days-selection').hide();

View File

@@ -13,23 +13,29 @@
<div class="header-bar">
<span><i class="fa fa-history"></i> History</span>
</div>
<div class="colvis-button-bar hidden-xs">
<div class="button-bar">
<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'>
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete_row">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody>
@@ -39,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>
@@ -60,9 +84,47 @@
}
}
history_table = $('#history_table').DataTable(history_table_options);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar');
});
clearSearchButton('history_table', history_table);
$('#row-edit-mode').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() {
@@ -43,6 +44,34 @@
}
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

@@ -9,148 +9,642 @@ Variable names: data [array]
data[array_index] :: Usable parameters
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'popular_tv', 'top_user' or 'top_platform'
data['stat_id'] Returns the name of the stat. Either 'top_tv', 'top_movies', 'popular_tv', 'popular_movies', 'top_user' or 'top_platform'
data['stat_type'] Returns the type of the stat. Either 'total_plays' or 'total_duration'
data['rows'] Returns an array containing stat data
data[array_index]['rows'] :: Usable parameters
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.
title Returns the title for the associated stat.
== Only if 'stat_id' is 'top_tv' or 'top_user' or 'top_platform' ==
== Only if 'stat_id' is 'top_tv' or 'top_movies' or 'top_user' or 'top_platform' ==
total_plays Returns the count for the associated stat.
total_duration Returns the total duration for the associated stat.
== Only of 'stat_id' is 'popular_tv' ==
== Only of 'stat_id' is 'popular_tv' or 'popular_movies' ==
users_watched Returns the count for the associated stat.
== Only if 'stat_id' is 'top_user' ==
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>
% if data != None:
% if data[0]['rows']:
<%!
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) % 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>"
%>
% if data:
% 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':
% for top_stat in data:
% if top_stat['stat_id'] == 'top_tv' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<span>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb']:
<img class="home-platforms-instance-poster"
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Watched TV</h4>
</div>
<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:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
${top_stat['rows'][0]['total_duration'] | hd}
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Watched TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
</div>
</li>
</div>
% elif a['stat_id'] == 'popular_tv':
<div class="home-platforms-instance">
<li>
<span>
<a href="info?item_id=${a['rows'][0]['rating_key']}">
% if a['rows'][0]['grandparent_thumb'] != '':
<img class="home-platforms-instance-poster"
src="pms_image_proxy?img=${a['rows'][0]['grandparent_thumb']}&width=162&height=240&fallback=poster">
% else:
<img class="home-platforms-instance-poster" src="interfaces/default/images/poster.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Popular TV</h4>
<h5><a href="info?item_id=${a['rows'][0]['rating_key']}">
${a['rows'][0]['title']}
</a></h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['users_watched']}</h3>
<p> users</p>
</div>
</li>
</div>
% elif a['stat_id'] == 'top_users':
<div class="home-platforms-instance">
<li>
<span>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
<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="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="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
% if a['rows'][0]['thumb'] != '':
<img class="home-platforms-instance-oval" src="${a['rows'][0]['thumb']}"
class="poster-face">
% endfor
</div>
</div>
</ul>
% endif
</li>
</div>
% 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>
</div>
<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=${top_stat['rows'][0]['rating_key']}">
% if top_stat['rows'][0]['grandparent_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]['grandparent_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?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 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>
</div>
<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:
${top_stat['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
<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="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?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 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>
</div>
<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=${top_stat['rows'][0]['rating_key']}">
% 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?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 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>
</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=${top_stat['rows'][0]['user']}">
% endif
${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="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>
% 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
% if top_stat['rows'][loop.index]['user_thumb'] != '':
<div class="home-platforms-instance-poster">
<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-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 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>
</div>
<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:
${top_stat['rows'][0]['total_duration'] | hd}
% endif
</div>
</div>
<div id="platform-stat" class="home-platforms-instance-poster">
<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:
<img class="home-platforms-instance-oval"
src="interfaces/default/images/gravatar-default.png">
% endif
</a>
</span>
<div class="home-platforms-instance-name">
<h4>Most Active User</h4>
<h5>
% if a['rows'][0]['user_id']:
<a href="user?user_id=${a['rows'][0]['user_id']}">
% else:
<a href="user?user=${a['rows'][0]['user']}">
% endif
${a['rows'][0]['friendly_name']}
</a>
</h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
<a href="user?user=${top_stat['rows'][0]['user']}">
% endif
${top_stat['rows'][0]['friendly_name']}
</a>
</h5>
<p>
<span id="last-watch-stat">
<script>
$('#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>
% elif a['stat_id'] == 'top_platforms':
<div class="home-platforms-instance">
<li>
<div id="platform-stat">
<img class="home-platforms-instance-box" src="interfaces/default/images/platforms/default.png">
</div>
<div class="home-platforms-instance-name">
<h4>Most Active Platform</h4>
<h5>${a['rows'][0]['platform_type']}</h5>
</div>
<div class="user-platforms-instance-playcount">
<h3>${a['rows'][0]['total_plays']}</h3>
<p> plays</p>
</div>
</li>
</div>
<script>
$("#platform-stat").html("<img class='home-platforms-instance-box' src='" + getPlatformImagePath('${a['rows'][0]['platform_type']}') + "'>");
</script>
% 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

@@ -19,9 +19,20 @@
<div class="row">
<div class="col-md-12">
<div class="padded-header">
<h3>Statistics <small>Last 30 days</small></h3>
<h3>Watch Statistics <small>Last ${config['home_stats_length']} days</small></h3>
</div>
<div id="home-stats" class="user-platforms">
<div id="home-stats" class="home-platforms">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
<br>
</div>
</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>
@@ -45,17 +56,18 @@
<script src="interfaces/default/js/moment-with-locale.js"></script>
<script>
function getHomeStats(days) {
function currentActivityHeader() {
$.ajax({
url: 'home_stats',
url: 'get_current_activity_header',
cache: false,
async: true,
data: {time_range: days},
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(30);
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

@@ -30,6 +30,7 @@ genres Returns an array of genres.
actors Returns an array of actors.
directors Returns an array of directors.
studio Returns the name of the studio.
originally_available_at Returns the air date of the item.
DOCUMENTATION :: END
</%doc>
@@ -46,169 +47,225 @@ 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="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['type'] == 'episode':
<img src="pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster">
% else:
<img src="pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster">
% endif
</div>
<div class="summary-content">
<div class="summary-content-title">
% if data['type'] == 'movie':
<h1>${data['title']} (${data['year']})</h1>
% elif data['type'] == 'season':
<h1>${data['parent_title']} (${data['title']})</h1>
% elif data['type'] == 'episode':
<h1>${data['grandparent_title']} (Season ${data['parent_index']}, Episode
${data['index']}) "${data['title']}"</h1>
% else:
<h1>${data['title']}</h1>
% endif
</div>
% if data['type'] == 'movie':
<div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
data-rateit-ispreset="true" data-rateit-readonly="true"></div>
% endif
<div class="summary-content-details-wrapper">
<div class="summary-content-director">
% if data['type'] == 'episode' or data['type'] == 'movie':
% if data['directors']:
Directed by <strong> ${data['directors'][0]}</strong>
% else:
Directed by <strong> unknown</strong>
% endif
% elif data['type'] == 'show' or data['type'] == 'season':
Studio <strong> ${data['studio']}</strong>
% endif
</div>
<div class="summary-content-duration">
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
</div>
<div class="summary-content-content-rating">
Rated <strong> ${data['content_rating']} </strong>
</div>
</div>
<div class="summary-content-summary">
<p> ${data['summary']} </p>
</div>
</div>
</div>
% if data['type'] == 'episode':
<div class="col-md-3">
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
<div class="summary-content-writers">
<strong>Written by</strong>
<ul>
% for writer in data['writers']:
% if loop.index < 5:
<li>
${writer}
</li>
% endif
% endfor
</ul>
</div>
</div>
</div>
% elif data['type'] == 'movie' or data['type'] == 'show':
<div class="col-md-3">
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
<div class="summary-content-actors">
<strong>Genres</strong>
<ul>
% for genre in data['genres']:
% if loop.index < 5:
<li>
${genre}
</li>
% endif
% endfor
</ul>
</div>
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
<div class="summary-content-actors">
<strong>Starring</strong>
<ul>
% for actor in data['actors']:
% if loop.index < 5:
<li>
${actor}
</li>
% endif
% endfor
</ul>
</div>
</div>
</div>
</div>
% elif data['type'] == 'season':
<div class="col-md-3"></div>
% endif
</div>
<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>TV Shows</span>
<span><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>TV Shows</span>
<span><i class="fa fa-chevron-right"></i></span>
<span><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></span>
<span><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>
% endif
</div>
</div>
</div>
</div>
</div>
</div>
% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show':
<div class='container-fluid'>
<div class='row'>
<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="colvis-button-bar hidden-xs">
<div class="summary-content-title-wrapper">
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% 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>
% elif data['type'] == 'season':
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"></div>
% else:
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"></div>
% endif
</div>
<div class="summary-content-title">
% if data['type'] == 'movie' or data['type'] == 'show':
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['type'] == 'season':
<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><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>
% endif
</div>
</div>
</div>
<div class="table-card-back">
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<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="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th>
</tr>
</thead>
<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>
</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 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>
% else:
<div class="summary-content-padding hidden-xs hidden-sm"></div>
% endif
<div class="summary-content">
<div class="summary-content-details-wrapper">
% if (data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode') and data['rating']:
<div id="stars" class="rateit hidden-xs hidden-sm" data-rateit-value=""
data-rateit-ispreset="true" data-rateit-readonly="true"></div>
% endif
<div class="summary-content-director">
% if (data['type'] == 'episode' or data['type'] == 'movie') and 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']:
Studio <strong> ${data['studio']}</strong>
% endif
</div>
<div class="summary-content-airdate">
% if data['type'] == 'movie':
Year <strong> ${data['year']}</strong>
% elif data['type'] == 'show':
Aired <strong> ${data['year']}</strong>
% elif data['type'] == 'episode':
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% endif
</div>
<div class="summary-content-duration">
Runtime <strong> <span id="runtime">${data['duration']}</span> mins</strong>
</div>
<div class="summary-content-content-rating">
% if (data['type'] == 'episode' or data['type'] == 'movie' or data['type'] == 'show') and data['content_rating']:
Rated <strong> ${data['content_rating']} </strong>
% endif
</div>
</div>
% if data['type'] == 'movie' and 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>
</div>
</div>
<div class="col-md-3">
<div class="summary-content-people-wrapper hidden-xs hidden-sm">
% if (data['type'] == 'episode' or data['type'] == 'movie') and data['writers']:
<div class="summary-content-writers">
<strong>Written by</strong>
<ul>
% for writer in data['writers']:
% if loop.index < 5:
<li>
${writer}
</li>
% endif
% endfor
</ul>
</div>
% endif
% if (data['type'] == 'movie' or data['type'] == 'show') and data['actors']:
<div class="summary-content-actors">
<strong>Starring</strong>
<ul>
% for actor in data['actors']:
% if loop.index < 5:
<li>
${actor}
</li>
% endif
% endfor
</ul>
</div>
% endif
</div>
<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
</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="season-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>
<div class='table-card-back'>
<div id="episode-list"></div>
</div>
</div>
% endif
% if data['type'] == 'movie' or data['type'] == 'episode' or data['type'] == 'show' or data['type'] == 'season':
<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">
<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">
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<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>
% endif
</div>
<div class='table-card-back'>
<div id="episode-list"></div>
</div>
% endif
</div>
</div>
</div>
@@ -216,8 +273,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>
@@ -233,7 +292,7 @@ DOCUMENTATION :: END
<script src="interfaces/default/js/moment-with-locale.js"></script>
% if data:
% if data['type'] == 'movie':
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'episode':
<script>
// Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2)
@@ -249,15 +308,27 @@ DOCUMENTATION :: END
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'rating_key': ${data['rating_key']}
};
'rating_key': ${data['rating_key']} };
}
}
history_table = $('#history_table').DataTable(history_table_options);
history_table.column(4).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table);
$('#row-edit-mode').click(function() {
if ($(this).hasClass('active')) {
$('.delete-control').each(function() {
$(this).addClass('hidden');
});
} else {
$('.delete-control').each(function() {
$(this).removeClass('hidden');
});
}
});
});
</script>
% elif data['type'] == 'show':
@@ -269,32 +340,134 @@ DOCUMENTATION :: END
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'grandparent_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: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('div.colvis-button-bar');
});
</script>
% endif
% if data['type'] == 'season':
<script>
clearSearchButton('history_table', history_table);
$('#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');
});
}
});
});
$.ajax({
url: 'get_children',
url: 'get_show_children',
type: "GET",
async: true,
data: { rating_key : ${data['rating_key']} },
complete: function(xhr, status) {
$("#episode-list").html(xhr.responseText);
$("#season-list").html(xhr.responseText); }
});
</script>
% endif
% if data['type'] == 'season':
<script src="interfaces/default/js/tables/history_table.js"></script>
<script>
$(document).ready(function () {
history_table_options.ajax = {
"url": "get_history",
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'parent_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').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');
});
}
});
});
$.ajax({
url: 'get_season_children',
type: "GET",
async: true,
data: { rating_key : ${data['rating_key']} },
complete: function(xhr, status) {
$("#episode-list").html(xhr.responseText); }
});
</script>
% endif
<script>
$("#runtime").html(millisecondsToMinutes($("#runtime").html(), true));
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
</script>
% endif
<script>
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
</script>
</%def>

View File

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

View File

@@ -0,0 +1,55 @@
<%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_season_list.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
season_count Returns the number of seasons in the array.
season_list Returns an array of seasons.
data['season_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 season.
index Returns the season number.
DOCUMENTATION :: END
</%doc>
% if data != None:
% if data['season_count'] > 0:
<div class="show-seasons-wrapper">
<ul class="show-seasons-instance list-unstyled">
% for a in data['season_list']:
% if a['rating_key']:
<li>
<a href="info?item_id=${a['rating_key']}">
<div class="show-seasons-poster">
% if a['thumb']:
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['thumb']}&width=300&height=450);">
% else:
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${a['parent_thumb']}&width=300&height=450);">
% endif
<div class="show-seasons-card-overlay">
<div class="show-seasons-overlay-text">
Season ${a['index']}
</div>
</div>
</div>
</div>
</a>
</li>
% endif
% endfor
</ul>
</div>
% endif
% endif

View File

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

View File

@@ -25,11 +25,7 @@ var hc_plays_by_month_options = {
},
colors: ['#F9AA03', '#FFFFFF'],
xAxis: {
type: 'datetime',
labels: {
formatter: function() {
return moment(this.value).format("MMM YYYY");
},
style: {
color: '#aaa'
}

View File

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

View File

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

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

View File

@@ -1,3 +1,5 @@
var users_to_purge = [];
users_list_table_options = {
"language": {
"search": "Search: ",
@@ -11,57 +13,150 @@ 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": "thumb",
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === '') {
$(td).html('<img src="interfaces/default/images/gravatar-default-80x80.png" alt="User Logo"/>');
} else {
$(td).html('<img src="' + cellData + '" alt="User Logo"/>');
}
$(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>');
},
"orderable": false,
"className": "users-poster-face",
"width": "40px"
"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('<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('<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-thumbs"
},
{
"targets": [2],
"data": "friendly_name",
"createdCell": function (td, cellData, rowData, row, col) {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id'] > 0) {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>');
$(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);
}
},
},
{
"targets": [2],
"data": "started",
"render": function ( data, type, full ) {
return moment(data, "X").fromNow();
},
"className": "hidden-xs",
"width": "12%",
"className": "edit-user-control no-wrap"
},
{
"targets": [3],
"data": "ip_address",
"data": "last_seen",
"render": function ( data, type, full ) {
if (data) {
return moment(data, "X").fromNow();
} else {
return "never";
}
},
"searchable": false,
"className": "hidden-xs",
"width": "12%",
"className": "no-wrap hidden-xs"
},
{
"targets": [4],
"data": "plays"
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
if (isPrivateIP(cellData)) {
if (cellData != '') {
$(td).html(cellData);
} else {
$(td).html('n/a');
}
} else {
$(td).html('<a href="javascript:void(0)" data-toggle="modal" data-target="#ip-info-modal"><span data-toggle="ip-tooltip" data-placement="left" title="IP Address Info" id="ip-info"><i class="fa fa-map-marker"></i></span>&nbsp' + cellData + '</a>');
}
} else {
$(td).html('n/a');
}
},
"width": "12%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control-ip"
},
{
"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>';
} 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>');
} else {
$(td).html('n/a');
}
},
"width": "12%",
"className": "no-wrap hidden-md hidden-sm hidden-xs modal-control"
},
{
"targets": [6],
"data":"last_watched",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
var media_type = '';
var thumb_popover = ''
if (rowData['media_type'] === 'movie') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=120&fallback=poster" data-height="120">' + cellData + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&item_id=' + rowData['id'] + '"><div style="float: left;" >' + media_type + '&nbsp' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=80&height=80&fallback=poster" data-height="80">' + cellData + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp' + thumb_popover + '</div></div>');
} else if (rowData['media_type']) {
$(td).html('<a href="info?item_id=' + rowData['id'] + '">' + cellData + '</a>');
} else {
$(td).html('n/a');
}
}
},
"width": "30%",
"className": "hidden-sm hidden-xs"
},
{
"targets": [7],
"data": "plays",
"searchable": false,
"width": "10%"
}
],
@@ -69,9 +164,125 @@ users_list_table_options = {
// Jump to top of page
//$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('.purge-tooltip').tooltip();
$('.edit-tooltip').tooltip();
$('.transcode-tooltip').tooltip();
$('.media-type-tooltip').tooltip();
$('.watched-tooltip').tooltip();
$('.thumb-tooltip').popover({
html: true,
trigger: 'hover',
placement: 'right',
content: function () {
return '<div style="background-image: url(' + $(this).data('img') + '); width: 80px; height: ' + $(this).data('height') + 'px;" />';
}
});
if ($('#row-edit-mode').hasClass('active')) {
$('.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');
}
}
}
$('#users_list_table').on('click', 'td.modal-control', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
function showStreamDetails() {
$.ajax({
url: 'get_stream_data',
data: { row_id: rowData['id'], user: rowData['friendly_name'] },
cache: false,
async: true,
complete: function (xhr, status) {
$("#info-modal").html(xhr.responseText);
}
});
}
showStreamDetails();
});
$('#users_list_table').on('click', 'td.modal-control-ip', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
function getUserLocation(ip_address) {
if (isPrivateIP(ip_address)) {
return "n/a"
} else {
$.ajax({
url: 'get_ip_address_details',
data: { ip_address: ip_address },
async: true,
complete: function (xhr, status) {
$("#ip-info-modal").html(xhr.responseText);
}
});
}
}
getUserLocation(rowData['ip_address']);
});
$('#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

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

View File

@@ -0,0 +1,98 @@
<%!
from plexpy import helpers
%>
% if data:
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title" id="notification-triggers-modal-header">${data['name']} Notification Triggers</h4>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<p class="help-block">
Watched notifications are only applicable for video items.
</p>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_play" ${helpers.checked(data['on_play'])} class="toggle-switches">
Notify on playback start
</label>
<p class="help-block">Trigger notification when a new media item is started.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_stop" ${helpers.checked(data['on_stop'])} class="toggle-switches">
Notify on playback stop
</label>
<p class="help-block">Trigger notification when a media item is stopped.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_pause" ${helpers.checked(data['on_pause'])} class="toggle-switches">
Notify on playback pause
</label>
<p class="help-block">Trigger notification when a media item is paused.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_resume" ${helpers.checked(data['on_resume'])} class="toggle-switches">
Notify on playback resume
</label>
<p class="help-block">Trigger notification when a media item is resumed from a paused state.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_watched" ${helpers.checked(data['on_watched'])} class="toggle-switches">
Notify on watched
</label>
<p class="help-block">Trigger notification when a video item reaches the defined watch percentage.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_buffer" ${helpers.checked(data['on_buffer'])} class="toggle-switches">
Notify on buffer warning
</label>
<p class="help-block">Trigger notification when a media item triggers the defined buffer threshold.</p>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<input type="button" class="btn btn-bright" data-dismiss="modal" value="Close">
</div>
</div>
</div>
<script>
$('.toggle-switches').click(function() {
var configToggle = $(this).data('id');
var toggle = $(this);
if ($(this).is(":checked")) {
var data = {};
data[$(this).data('config-name')] = 1;
$.ajax({
url: 'set_notification_config',
data: data,
async: true,
success: function(data) {
console.log('success');
}
});
} else {
var data = {};
data[$(this).data('config-name')] = 0;
$.ajax({
url: 'set_notification_config',
data: data,
async: true,
success: function(data) {
console.log('success');
}
});
}
});
</script>
% endif

View File

@@ -15,6 +15,7 @@ type Returns the type of media. Either 'movie' or 'season'.
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
added_at Returns the time when the media was added to the library.
title Returns the name of the movie or season.
parent_title Returns the name of the TV Show a season belongs too.
== Only if 'type' is 'movie' ==
year Returns the movie release year.
@@ -28,27 +29,32 @@ DOCUMENTATION :: END
% for item in data:
<div class="dashboard-recent-media-instance">
<li>
<div class="poster">
% if item['type'] == 'season' or item['type'] == 'movie':
<div class="poster-face">
<a href="info?item_id=${item['rating_key']}">
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster" class="poster-face">
</a>
% if item['type'] == 'season' or item['type'] == 'movie':
<a href="info?item_id=${item['rating_key']}">
<div class="poster">
<div class="poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);"></div>
</div>
% elif item['type'] == 'album':
<div class="cover-face">
<img src="pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover" class="cover-face">
</div>
% endif
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season' or item['type'] == 'album':
<h3>${item['title']}</h3>
% elif item['type'] == 'movie':
<h3>${item['title']} (${item['year']})</h3>
% endif
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season':
<h3>${item['parent_title']}</h3>
<h3>(${item['title']})</h3>
% elif item['type'] == 'movie':
<h3>${item['title']}</h3>
<h3>(${item['year']})</h3>
% endif
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
</a>
% elif item['type'] == 'album':
<div class="poster">
<div class="cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"></div>
</div>
<div class="dashboard-recent-media-metacontainer">
<h3>${item['parent_title']}</h3>
<h3>${item['title']}</h3>
<div class="text-muted" id="added_at-${item['rating_key']}">${item['added_at']}</div>
</div>
% endif
</li>
</div>
<script>

View File

@@ -1,10 +1,12 @@
<%inherit file="base.html"/>
<%!
import plexpy
from plexpy import notifiers
from plexpy import notifiers, common, versioncheck
available_notification_agents = notifiers.available_notification_agents()
%>
<%def name="headIncludes()">
</%def>
<%def name="headerIncludes()">
</%def>
@@ -46,9 +48,11 @@ available_notification_agents = notifiers.available_notification_agents()
<form action="configUpdate" method="post" class="form" id="configUpdate" data-parsley-validate>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-1">
% if common.VERSION_NUMBER:
<div class="padded-header">
<h3>Software Updates</h3>
<h3>Version ${common.VERSION_NUMBER} <small><a href="#changelog-modal" data-toggle="modal"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
</div>
% endif
<div class="checkbox">
<label>
<input type="checkbox" id="check_github" name="check_github" value="1" ${config['check_github']}> Enable Updates
@@ -56,7 +60,7 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">If you have Git installed, allow periodic checks for updates.</p>
</div>
% if plexpy.CURRENT_VERSION:
<p>Current version: ${plexpy.CURRENT_VERSION}</p>
<p class="help-block">Git hash: ${plexpy.CURRENT_VERSION}</p>
% endif
<div class="padded-header">
<h3>Display Settings</h3>
@@ -79,7 +83,37 @@ 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>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<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">
<div class="padded-header">
@@ -99,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>
@@ -127,7 +162,7 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
</div>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<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-3">
@@ -179,7 +214,7 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">Current API key: <strong><br/>${config['api_key']}</strong></p>
</div>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<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-4">
@@ -200,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>
@@ -217,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">
@@ -248,8 +299,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>
@@ -260,7 +312,7 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">Refresh the user list when PlexPy starts.</p>
</div>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<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-6">
<div class="padded-header">
@@ -269,32 +321,17 @@ 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>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>
<p class="help-block"><a href="javascript:void(0)" id="toggle-plexwatch-import-modal" data-target="#plexwatch-import-modal" data-toggle="modal">Click here to Import an existing Plexwatch database.</a></p>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<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-7">
@@ -305,10 +342,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">
@@ -321,11 +359,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>
@@ -349,7 +388,32 @@ available_notification_agents = notifiers.available_notification_agents()
</p>
</div>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<div class="padded-header">
<h3>Buffer Warnings</h3>
</div>
<p class="help-block">Note: Buffer warnings only work on certain Plex clients. Android and PlexWeb do not report buffer events accurately or at all.</p>
<div class="form-group">
<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" 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>
<div class="form-group">
<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" 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>
<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-8">
@@ -375,8 +439,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>
@@ -428,6 +493,40 @@ available_notification_agents = notifiers.available_notification_agents()
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-pause"></i>Playback Pause<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_pause_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_pause_subject_text" name="notify_on_pause_subject_text" value="${config['notify_on_pause_subject_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom subject line.</p>
</div>
<div class="form-group">
<label for="notify_on_pause_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_pause_body_text" name="notify_on_pause_body_text" value="${config['notify_on_pause_body_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-play"></i>Playback Resume<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_resume_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_resume_subject_text" name="notify_on_resume_subject_text" value="${config['notify_on_resume_subject_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom subject line.</p>
</div>
<div class="form-group">
<label for="notify_on_resume_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_resume_body_text" name="notify_on_resume_body_text" value="${config['notify_on_resume_body_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-eye"></i>Watched<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
@@ -445,9 +544,26 @@ available_notification_agents = notifiers.available_notification_agents()
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-spinner"></i>Buffer Warnings<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_buffer_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_buffer_subject_text" name="notify_on_buffer_subject_text" value="${config['notify_on_buffer_subject_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom subject line.</p>
</div>
<div class="form-group">
<label for="notify_on_buffer_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_buffer_body_text" name="notify_on_buffer_body_text" value="${config['notify_on_buffer_body_text']}" data-parsley-trigger="change" required>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
</ul>
<input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully">
<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-9">
@@ -455,18 +571,18 @@ available_notification_agents = notifiers.available_notification_agents()
<h3>Notification Agents</h3>
</div>
<p class="help-block">
Toggle the desired notification option and configure it by selecting the settings icon to the right.
Watched notifications are only applicable for video items.
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">
% for agent in available_notification_agents:
<li>
<span>
<!--<input type="checkbox" name="${agent['config_prefix']}_enabled" id="${agent['config_prefix']}" value="1" ${agent['state']}> ${agent['name']}-->
<a class="toggle-left notify-toggle-icon" href="javascript:void(0)" data-toggle="tooltip" data-placement="top" title data-title="Notify on playback start" data-id="${agent['id']}" data-config-name="${agent['config_prefix']}_on_play" data-config-value="${agent['on_play']}"><i class="fa fa-play"></i></a>
<a class="toggle-left notify-toggle-icon" href="javascript:void(0)" data-toggle="tooltip" data-placement="top" title data-title="Notify on stop" data-id="${agent['id']}" data-config-name="${agent['config_prefix']}_on_stop" data-config-value="${agent['on_stop']}"><i class="fa fa-stop"></i></a>
<a class="toggle-left notify-toggle-icon" href="javascript:void(0)" data-toggle="tooltip" data-placement="top" title data-title="Notify on watched" data-id="${agent['id']}" data-config-name="${agent['config_prefix']}_on_watched" data-config-value="${agent['on_watched']}"><i class="fa fa-eye"></i></a>
% 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-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-bell"></i></a>
% endif
${agent['name']}
% if agent['has_config']:
<a href="javascript:void(0)" rel="tooltip" data-target="#notification-config-modal" data-placement="top" title data-title="Open configuration" data-id="${agent['id']}" class="toggle-notification-config-modal toggle-right" data-toggle="modal"><i class="fa fa-lg fa-cog"></i></a>
@@ -651,7 +767,8 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
</div>
<div id="plexwatch-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="plexwatch-import-modal"></div>
<div id="notification-config-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="notification-setting-modal"></div>
<div id="notification-config-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="notification-config-modal"></div>
<div id="notification-triggers-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="notification-triggers-modal"></div>
<div id="notify-text-sub-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="notify-text-sub-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
@@ -723,10 +840,18 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{season_num}</strong></td>
<td>The season number for the media item if item is episode.</td>
</tr>
<tr>
<td width="150"><strong>{season_num00}</strong></td>
<td>The two digit season number.</td>
</tr>
<tr>
<td width="150"><strong>{episode_num}</strong></td>
<td>The episode number for the media item if item is episode.</td>
</tr>
<tr>
<td width="150"><strong>{episode_num00}</strong></td>
<td>The two digit episode number.</td>
</tr>
<tr>
<td width="150"><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</td>
@@ -735,6 +860,10 @@ available_notification_agents = notifiers.available_notification_agents()
<td width="150"><strong>{duration}</strong></td>
<td>The duration (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{stream_duration}</strong></td>
<td>The stream duration (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{progress}</strong></td>
<td>The last reported offset (in minutes) for the item.</td>
@@ -788,6 +917,21 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
</div>
</div>
<div id="changelog-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Changelog</h4>
</div>
<div class="modal-body">
${versioncheck.read_changelog()}
</div>
<div class="modal-footer">
</div>
</div>
</div>
</div>
</div>
</%def>
@@ -834,6 +978,8 @@ $(document).ready(function() {
});
$("#menu_link_update_check").click(function() {
// Allow the update bar to show again if previously dismissed.
setCookie('updateDismiss', 'true', 0);
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking</button>');
$(this).prop('disabled', true);
window.location.href = "checkGithub";
@@ -934,7 +1080,9 @@ $(document).ready(function() {
headers: {'Content-Type': 'application/xml; charset=utf-8',
'X-Plex-Device-Name': 'PlexPy',
'X-Plex-Product': 'PlexPy',
'X-Plex-Version': 'v0.1 dev',
'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
},
@@ -965,7 +1113,7 @@ $(document).ready(function() {
});
});
// Load PlexWatch import modal
// Load notification agent config modal
$(".toggle-notification-config-modal").click(function() {
var configId = $(this).data('id');
$.ajax({
@@ -979,100 +1127,18 @@ $(document).ready(function() {
});
});
$('.notify-toggle-icon').tooltip();
$('.notify-toggle-icon').each(function() {
if ($(this).data('config-value') == 1) {
$(this).addClass("active");
}
});
$('.notify-toggle-icon').click(function() {
var configToggle = $(this).data('id');
var toggle = $(this);
if ($(this).hasClass("active")) {
var data = {};
data[$(this).data('config-name')] = 0;
$.ajax({
url: 'set_notification_config',
data: data,
async: true,
success: function(data) {
toggle.removeClass("active");
}
});
} else {
var data = {};
data[$(this).data('config-name')] = 1;
$.ajax({
url: 'set_notification_config',
data: data,
async: true,
success: function(data) {
toggle.addClass("active");
}
});
}
});
if ($("#tv_notify_enable").is(":checked"))
{
$("#tv_notify_options").show();
}
else
{
$("#tv_notify_options").hide();
}
$("#tv_notify_enable").click(function(){
if ($("#tv_notify_enable").is(":checked"))
{
$("#tv_notify_options").slideDown();
}
else
{
$("#tv_notify_options").slideUp();
}
});
if ($("#movie_notify_enable").is(":checked"))
{
$("#movie_notify_options").show();
}
else
{
$("#movie_notify_options").hide();
}
$("#movie_notify_enable").click(function(){
if ($("#movie_notify_enable").is(":checked"))
{
$("#movie_notify_options").slideDown();
}
else
{
$("#movie_notify_options").slideUp();
}
});
if ($("#music_notify_enable").is(":checked"))
{
$("#music_notify_options").show();
}
else
{
$("#music_notify_options").hide();
}
$("#music_notify_enable").click(function(){
if ($("#music_notify_enable").is(":checked"))
{
$("#music_notify_options").slideDown();
}
else
{
$("#music_notify_options").slideUp();
// Load notification triggers config modal
$(".toggle-notification-triggers-modal").click(function() {
var configId = $(this).data('id');
$.ajax({
url: 'get_notification_agent_triggers',
data: { config_id: configId },
cache: false,
async: true,
complete: function(xhr, status) {
$("#notification-triggers-modal").html(xhr.responseText);
}
});
});
$('#osxnotifyregister').click(function () {
@@ -1113,4 +1179,4 @@ $(document).ready(function() {
var accordion = new Accordion($('#accordion'), false);
});
</script>
</%def>
</%def>

View File

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

View File

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

View File

@@ -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 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>
@@ -103,7 +102,7 @@ from plexpy import helpers
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span><i class="fa fa-history"></i> Recently watched</span>
<span><i class="fa fa-history"></i> Recently Watched</span>
</div>
</div>
<div class="table-card-back">
@@ -131,17 +130,15 @@ from plexpy import helpers
<table id="user_ip_table" class="display" width="100%">
<thead>
<tr>
<th align="left">Last seen</th>
<th align="left">Last Seen</th>
<th align="left">IP Address</th>
<th align="left">Play Count</th>
<th align="left">Platform (Last Seen)</th>
<th align="left">Last Platform</th>
<th align="left">Last Watched</th>
<th align="left">Play Count</th>
</tr>
</thead>
</table>
</div>
<div id="ip-info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
</div>
</div>
</div>
@@ -156,30 +153,35 @@ from plexpy import helpers
<span class="set-username">${data['friendly_name']}</span>
</strong></span>
</div>
<div class="colvis-button-bar hidden-xs" id="button-bar-history">
<div class="button-bar">
<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">
<table class="display" id="history_table" width="100%">
<thead>
<tr>
<th align='left' id="delete">Delete</th>
<th align='left' id="time">Time</th>
<th align='left' id="friendly_name">User</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="ip_address">IP Address</th>
<th align='left' id="platform">Platform</th>
<th align='left' id="title">Title</th>
<th align='left' id="started">Started</th>
<th align='left' id="paused_counter">Paused</th>
<th align='left' id="stopped">Stopped</th>
<th align='left' id="duration">Duration</th>
<th align='left' id="percent_complete">Watched</th>
<th align='left' id="percent_complete"></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal"></div>
</div>
</div>
</div>
@@ -222,6 +224,28 @@ from plexpy import helpers
</div>
</div>
</div>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
<div 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>
@@ -236,6 +260,33 @@ from plexpy import helpers
<script src="interfaces/default/js/tables/user_ips.js"></script>
<script src="interfaces/default/js/tables/sync_table.js"></script>
<script>
function recentlyWatched() {
var widthVal = $('body').find("#user-recently-watched").width();
var tmp = (widthVal-32) / 180;
if (tmp > 0) {
containerSize = parseInt(tmp);
} else {
containerSize = 1;
}
% if data['user_id']:
var user_id = ${data['user_id']};
% else:
var user_id = null;
% endif
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: { user_id: user_id, user: '${data['username']}', limit: containerSize },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
}
$(document).ready(function () {
% if data['user_id']:
@@ -266,16 +317,6 @@ from plexpy import helpers
}
});
// Populate recently watched
$.ajax({
url: 'get_user_recently_watched',
async: true,
data: { user_id: user_id, user: '${data['username']}' },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
$( "#history-tab-btn" ).one( "click", function() {
// Build watch history table
history_table_options.ajax = {
@@ -289,10 +330,12 @@ from plexpy import helpers
}
}
history_table = $('#history_table').DataTable(history_table_options);
history_table.column(1).visible(false);
history_table.column(2).visible(false);
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' });
var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 10] });
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table);
});
$( "#ip-tab-btn" ).one( "click", function() {
@@ -308,6 +351,8 @@ from plexpy import helpers
}
}
user_ip_table = $('#user_ip_table').DataTable(user_ip_table_options);
clearSearchButton('user_ip_table', user_ip_table);
});
$( "#sync-tab-btn" ).one( "click", function() {
@@ -320,10 +365,12 @@ from plexpy import helpers
}
}
sync_table = $('#sync_table').DataTable(sync_table_options);
history_table.column(1).visible(false);
sync_table.column(1).visible(false);
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: 'Select columns', buttonClass: 'btn btn-dark' } );
var colvis_sync = new $.fn.dataTable.ColVis( sync_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' } );
$( colvis_sync.button() ).appendTo('#button-bar-sync');
clearSearchButton('sync_table', sync_table);
});
// Load edit user modal
@@ -339,6 +386,48 @@ from plexpy import helpers
}
});
});
$('#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');
});
}
});
recentlyWatched();
$(window).resize(function() {
recentlyWatched();
});
});
</script>
</%def>

View File

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

View File

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

View File

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

View File

@@ -9,26 +9,56 @@
<div class='container-fluid'>
<div class='table-card-header'>
<div class="header-bar">
<span><i class="fa fa-group"></i> Active Users</span>
<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>
<th align="left" id="last_known_ip">Last Known IP</th>
<th align="left" id="last_platform">Last Platform</th>
<th align="left" id="last_watched">Last Watched</th>
<th align="left" id="total_plays">Total Plays</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
<div 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>
@@ -41,15 +71,75 @@
<script src="interfaces/default/js/moment-with-locale.js"></script>
<script src="interfaces/default/js/tables/users.js"></script>
<script>
users_list_table_options.ajax = {
"url": "get_user_list",
type: "post",
data: function ( d ) {
return { 'json_data': JSON.stringify( d ) };
$(document).ready(function () {
users_list_table_options.ajax = {
"url": "get_user_list",
type: "post",
data: function ( d ) {
return { 'json_data': JSON.stringify( d ) };
}
}
}
var users_list_table = $('#users_list_table').DataTable(users_list_table_options);
users_list_table = $('#users_list_table').DataTable(users_list_table_options);
clearSearchButton('users_list_table', users_list_table);
$('#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({
@@ -65,4 +155,4 @@
});
});
</script>
</%def>
</%def>

View File

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

View File

@@ -1,4 +1,4 @@
# PlexPy - Automatic music downloader for SABnzbd
# PlexPy - 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 - Automatic music downloader for SABnzbd
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

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

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

View File

@@ -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 @@
# plexpy - Automatic music downloader
# plexpy
#
# This is a session/user job. Install this file into /usr/share/upstart/sessions
# if plexpy is installed system wide, and into $XDG_CONFIG_HOME/upstart if

View File

@@ -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
@@ -348,7 +348,8 @@ def dbcheck():
'bitrate INTEGER, video_resolution TEXT, video_framerate TEXT, aspect_ratio TEXT, '
'audio_channels INTEGER, transcode_protocol TEXT, transcode_container TEXT, '
'transcode_video_codec TEXT, transcode_audio_codec TEXT, transcode_audio_channels INTEGER,'
'transcode_width INTEGER, transcode_height INTEGER)'
'transcode_width INTEGER, transcode_height INTEGER, buffer_count INTEGER DEFAULT 0, '
'buffer_last_triggered INTEGER)'
)
# session_history table :: This is a history table which logs essential stream details
@@ -376,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)'
''
)
@@ -386,7 +387,8 @@ def dbcheck():
'CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, '
'user_id INTEGER DEFAULT NULL UNIQUE, username TEXT NOT NULL UNIQUE, '
'friendly_name TEXT, thumb TEXT, email TEXT, is_home_user INTEGER DEFAULT NULL, '
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1)'
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
'keep_history INTEGER DEFAULT 1, custom_avatar_url TEXT)'
)
# Upgrade sessions table from earlier versions
@@ -515,31 +517,92 @@ 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, '
'session_key INTEGER, rating_key INTEGER, user_id INTEGER, user TEXT, '
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER)'
'agent_id INTEGER, agent_name TEXT, on_play INTEGER, on_stop INTEGER, on_watched INTEGER, '
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER)'
)
# Upgrade sessions table from earlier versions
# Upgrade users table from earlier versions
try:
c_db.execute('SELECT do_notify from users')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
logger.debug(u"Altering database. Updating database table users.")
c_db.execute(
'ALTER TABLE users ADD COLUMN do_notify INTEGER DEFAULT 1'
)
# Upgrade users table from earlier versions
try:
c_db.execute('SELECT keep_history from users')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table users.")
c_db.execute(
'ALTER TABLE users ADD COLUMN keep_history INTEGER DEFAULT 1'
)
# Upgrade notify_log table from earlier versions
try:
c_db.execute('SELECT on_pause from notify_log')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table notify_log.")
c_db.execute(
'ALTER TABLE notify_log ADD COLUMN on_pause INTEGER'
)
c_db.execute(
'ALTER TABLE notify_log ADD COLUMN on_resume INTEGER'
)
c_db.execute(
'ALTER TABLE notify_log ADD COLUMN on_buffer INTEGER'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT buffer_count from sessions')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table sessions.")
c_db.execute(
'ALTER TABLE sessions ADD COLUMN buffer_count INTEGER DEFAULT 0'
)
c_db.execute(
'ALTER TABLE sessions ADD COLUMN buffer_last_triggered INTEGER'
)
# Upgrade users table from earlier versions
try:
c_db.execute('SELECT custom_avatar_url from users')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table users.")
c_db.execute(
'ALTER TABLE users ADD COLUMN custom_avatar_url TEXT'
)
# 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

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

View File

@@ -1,4 +1,4 @@
import plexpy.logger
import plexpy.logger
import itertools
import os
import re
@@ -39,7 +39,12 @@ _CONFIG_DEFINITIONS = {
'BOXCAR_TOKEN': (str, 'Boxcar', ''),
'BOXCAR_ON_PLAY': (int, 'Boxcar', 0),
'BOXCAR_ON_STOP': (int, 'Boxcar', 0),
'BOXCAR_ON_PAUSE': (int, 'Boxcar', 0),
'BOXCAR_ON_RESUME': (int, 'Boxcar', 0),
'BOXCAR_ON_BUFFER': (int, 'Boxcar', 0),
'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0),
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
'BUFFER_WAIT': (int, 'Monitoring', 900),
'CACHE_DIR': (str, 'General', ''),
'CACHE_SIZEMB': (int, 'Advanced', 32),
'CHECK_GITHUB': (int, 'General', 1),
@@ -58,6 +63,9 @@ _CONFIG_DEFINITIONS = {
'EMAIL_TLS': (int, 'Email', 0),
'EMAIL_ON_PLAY': (int, 'Email', 0),
'EMAIL_ON_STOP': (int, 'Email', 0),
'EMAIL_ON_PAUSE': (int, 'Email', 0),
'EMAIL_ON_RESUME': (int, 'Email', 0),
'EMAIL_ON_BUFFER': (int, 'Email', 0),
'EMAIL_ON_WATCHED': (int, 'Email', 0),
'ENABLE_HTTPS': (int, 'General', 0),
'FIRST_RUN_COMPLETE': (int, 'General', 0),
@@ -70,7 +78,13 @@ _CONFIG_DEFINITIONS = {
'GROWL_PASSWORD': (str, 'Growl', ''),
'GROWL_ON_PLAY': (int, 'Growl', 0),
'GROWL_ON_STOP': (int, 'Growl', 0),
'GROWL_ON_PAUSE': (int, 'Growl', 0),
'GROWL_ON_RESUME': (int, 'Growl', 0),
'GROWL_ON_BUFFER': (int, 'Growl', 0),
'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'),
@@ -100,18 +114,30 @@ _CONFIG_DEFINITIONS = {
'NMA_PRIORITY': (int, 'NMA', 0),
'NMA_ON_PLAY': (int, 'NMA', 0),
'NMA_ON_STOP': (int, 'NMA', 0),
'NMA_ON_PAUSE': (int, 'NMA', 0),
'NMA_ON_RESUME': (int, 'NMA', 0),
'NMA_ON_BUFFER': (int, 'NMA', 0),
'NMA_ON_WATCHED': (int, 'NMA', 0),
'NOTIFY_WATCHED_PERCENT': (int, 'Monitoring', 85),
'NOTIFY_ON_START_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_START_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) started playing {title}.'),
'NOTIFY_ON_STOP_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_STOP_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has stopped {title}.'),
'NOTIFY_ON_PAUSE_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_PAUSE_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has paused {title}.'),
'NOTIFY_ON_RESUME_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_RESUME_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has resumed {title}.'),
'NOTIFY_ON_BUFFER_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_BUFFER_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) is buffering {title}.'),
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (str, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_WATCHED_BODY_TEXT': (str, 'Monitoring', '{user} ({player}) has watched {title}.'),
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_STOP': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PAUSE': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_RESUME': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_BUFFER': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0),
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
'PLEX_ENABLED': (int, 'Plex', 0),
@@ -119,17 +145,26 @@ _CONFIG_DEFINITIONS = {
'PLEX_USERNAME': (str, 'Plex', ''),
'PLEX_ON_PLAY': (int, 'Plex', 0),
'PLEX_ON_STOP': (int, 'Plex', 0),
'PLEX_ON_PAUSE': (int, 'Plex', 0),
'PLEX_ON_RESUME': (int, 'Plex', 0),
'PLEX_ON_BUFFER': (int, 'Plex', 0),
'PLEX_ON_WATCHED': (int, 'Plex', 0),
'PROWL_ENABLED': (int, 'Prowl', 0),
'PROWL_KEYS': (str, 'Prowl', ''),
'PROWL_PRIORITY': (int, 'Prowl', 0),
'PROWL_ON_PLAY': (int, 'Prowl', 0),
'PROWL_ON_STOP': (int, 'Prowl', 0),
'PROWL_ON_PAUSE': (int, 'Prowl', 0),
'PROWL_ON_RESUME': (int, 'Prowl', 0),
'PROWL_ON_BUFFER': (int, 'Prowl', 0),
'PROWL_ON_WATCHED': (int, 'Prowl', 0),
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
'PUSHALOT_ON_STOP': (int, 'Pushalot', 0),
'PUSHALOT_ON_PAUSE': (int, 'Pushalot', 0),
'PUSHALOT_ON_RESUME': (int, 'Pushalot', 0),
'PUSHALOT_ON_BUFFER': (int, 'Pushalot', 0),
'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0),
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
@@ -137,6 +172,9 @@ _CONFIG_DEFINITIONS = {
'PUSHBULLET_ENABLED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_PLAY': (int, 'PushBullet', 0),
'PUSHBULLET_ON_STOP': (int, 'PushBullet', 0),
'PUSHBULLET_ON_PAUSE': (int, 'PushBullet', 0),
'PUSHBULLET_ON_RESUME': (int, 'PushBullet', 0),
'PUSHBULLET_ON_BUFFER': (int, 'PushBullet', 0),
'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0),
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
@@ -144,6 +182,9 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_PRIORITY': (int, 'Pushover', 0),
'PUSHOVER_ON_PLAY': (int, 'Pushover', 0),
'PUSHOVER_ON_STOP': (int, 'Pushover', 0),
'PUSHOVER_ON_PAUSE': (int, 'Pushover', 0),
'PUSHOVER_ON_RESUME': (int, 'Pushover', 0),
'PUSHOVER_ON_BUFFER': (int, 'Pushover', 0),
'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0),
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
@@ -164,6 +205,9 @@ _CONFIG_DEFINITIONS = {
'XBMC_USERNAME': (str, 'XBMC', ''),
'XBMC_ON_PLAY': (int, 'XBMC', 0),
'XBMC_ON_STOP': (int, 'XBMC', 0),
'XBMC_ON_PAUSE': (int, 'XBMC', 0),
'XBMC_ON_RESUME': (int, 'XBMC', 0),
'XBMC_ON_BUFFER': (int, 'XBMC', 0),
'XBMC_ON_WATCHED': (int, 'XBMC', 0)
}
# pylint:disable=R0902

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
@@ -26,67 +26,6 @@ class DataFactory(object):
def __init__(self):
pass
def get_user_list(self, kwargs=None):
data_tables = datatables.DataTables()
columns = ['session_history.id',
'users.thumb as thumb',
'(case when users.friendly_name is null then session_history.user else \
users.friendly_name end) as friendly_name',
'session_history.started',
'session_history.ip_address',
'COUNT(session_history.id) as plays',
'session_history.user',
'session_history.user_id',
'(case when typeof(session_history.user_id) = "integer" \
then session_history.user_id else -1 end) as grp_id',
]
try:
query = data_tables.ssp_query(table_name='session_history',
columns=columns,
custom_where=[],
group_by=['grp_id'],
join_types=['LEFT OUTER JOIN'],
join_tables=['users'],
join_evals=[['grp_id', 'users.user_id']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
users = query['result']
rows = []
for item in users:
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
row = {"id": item['id'],
"plays": item['plays'],
"started": item['started'],
"friendly_name": item["friendly_name"],
"ip_address": item["ip_address"],
"thumb": user_thumb,
"user": item["user"],
"user_id": item['user_id']
}
rows.append(row)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
def get_history(self, kwargs=None, custom_where=None):
data_tables = datatables.DataTables()
@@ -97,6 +36,13 @@ class DataFactory(object):
'session_history.player',
'session_history.ip_address',
'session_history_metadata.full_title as full_title',
'session_history_metadata.thumb',
'session_history_metadata.parent_thumb',
'session_history_metadata.grandparent_thumb',
'session_history_metadata.media_index',
'session_history_metadata.parent_media_index',
'session_history_metadata.parent_title',
'session_history_metadata.year',
'session_history.started',
'session_history.paused_counter',
'session_history.stopped',
@@ -109,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',
@@ -142,18 +89,31 @@ class DataFactory(object):
rows = []
for item in history:
if item["media_type"] == 'episode' and item["parent_thumb"]:
thumb = item["parent_thumb"]
elif item["media_type"] == 'episode':
thumb = item["grandparent_thumb"]
else:
thumb = item["thumb"]
row = {"id": item['id'],
"date": item['date'],
"friendly_name": item['friendly_name'],
"player": item["player"],
"ip_address": item["ip_address"],
"full_title": item["full_title"],
"thumb": thumb,
"media_index": item["media_index"],
"parent_media_index": item["parent_media_index"],
"parent_title": item["parent_title"],
"year": item["year"],
"started": item["started"],
"paused_counter": item["paused_counter"],
"stopped": item["stopped"],
"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"],
@@ -171,275 +131,19 @@ class DataFactory(object):
return dict
def get_user_unique_ips(self, kwargs=None, custom_where=None):
data_tables = datatables.DataTables()
columns = ['session_history.started as last_seen',
'session_history.ip_address as ip_address',
'COUNT(session_history.id) as play_count',
'session_history.player as platform',
'session_history_metadata.full_title as last_watched',
'session_history.user as user',
'session_history.user_id as user_id'
]
try:
query = data_tables.ssp_query(table_name='session_history',
columns=columns,
custom_where=custom_where,
group_by=['ip_address'],
join_types=['JOIN'],
join_tables=['session_history_metadata'],
join_evals=[['session_history.id', 'session_history_metadata.id']],
kwargs=kwargs)
except:
logger.warn("Unable to execute database query.")
return {'recordsFiltered': 0,
'recordsTotal': 0,
'draw': 0,
'data': 'null',
'error': 'Unable to execute database query.'}
results = query['result']
rows = []
for item in results:
row = {"last_seen": item['last_seen'],
"ip_address": item['ip_address'],
"play_count": item['play_count'],
"platform": item['platform'],
"last_watched": item['last_watched']
}
rows.append(row)
dict = {'recordsFiltered': query['filteredCount'],
'recordsTotal': query['totalCount'],
'data': rows,
'draw': query['draw']
}
return dict
# TODO: The getter and setter for this needs to become a config getter/setter for more than just friendlyname
def set_user_friendly_name(self, user=None, user_id=None, friendly_name=None, do_notify=0):
if user_id:
if friendly_name.strip() == '':
friendly_name = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"user_id": user_id}
new_value_dict = {"friendly_name": friendly_name,
"do_notify": do_notify}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
if user:
if friendly_name.strip() == '':
friendly_name = None
monitor_db = database.MonitorDatabase()
control_value_dict = {"username": user}
new_value_dict = {"friendly_name": friendly_name,
"do_notify": do_notify}
try:
monitor_db.upsert('users', new_value_dict, control_value_dict)
except Exception, e:
logger.debug(u"Uncaught exception %s" % e)
def get_user_friendly_name(self, user=None, user_id=None):
if user_id:
monitor_db = database.MonitorDatabase()
query = 'select username, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'do_notify ' \
'FROM users WHERE user_id = ?'
result = monitor_db.select(query, args=[user_id])
if result:
user_detail = {'user_id': user_id,
'user': result[0][0],
'friendly_name': result[0][1],
'do_notify': helpers.checked(result[0][2])}
return user_detail
else:
user_detail = {'user_id': user_id,
'user': '',
'friendly_name': '',
'do_notify': ''}
return user_detail
elif user:
monitor_db = database.MonitorDatabase()
query = 'select user_id, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'do_notify ' \
'FROM users WHERE username = ?'
result = monitor_db.select(query, args=[user])
if result:
user_detail = {'user_id': result[0][0],
'user': user,
'friendly_name': result[0][1],
'do_notify': helpers.checked(result[0][2])}
return user_detail
else:
user_detail = {'user_id': None,
'user': user,
'friendly_name': '',
'do_notify': ''}
return user_detail
return None
def get_user_id(self, user=None):
if user:
try:
monitor_db = database.MonitorDatabase()
query = 'select user_id FROM users WHERE username = ?'
result = monitor_db.select_single(query, args=[user])
if result:
return result
else:
return None
except:
return None
return None
def get_user_details(self, user=None, user_id=None):
from plexpy import plextv
monitor_db = database.MonitorDatabase()
if user:
query = 'SELECT user_id, username, friendly_name, email, ' \
'thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
query = 'SELECT user_id, username, friendly_name, email, ' \
'thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else:
result = None
if result:
user_details = {}
for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'],
"username": item['username'],
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
}
return user_details
else:
logger.warn(u"PlexPy :: Unable to retrieve user from local database. Requesting user list refresh.")
# Let's first refresh the user list to make sure the user isn't newly added and not in the db yet
if user:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE username = ? ' \
'UNION ALL ' \
'SELECT null, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user, user])
elif user_id:
# Refresh users
plextv.refresh_users()
query = 'SELECT user_id, username, friendly_name, email, ' \
'thumb, is_home_user, is_allow_sync, is_restricted, do_notify ' \
'FROM users ' \
'WHERE user_id = ? ' \
'UNION ALL ' \
'SELECT user_id, user, null, null, null, null, null, null, null ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY user ' \
'LIMIT 1'
result = monitor_db.select(query, args=[user_id, user_id])
else:
result = None
if result:
user_details = {}
for item in result:
if not item['friendly_name']:
friendly_name = item['username']
else:
friendly_name = item['friendly_name']
if not item['thumb'] or item['thumb'] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item['thumb']
user_details = {"user_id": item['user_id'],
"username": item['username'],
"friendly_name": friendly_name,
"email": item['email'],
"thumb": user_thumb,
"is_home_user": item['is_home_user'],
"is_allow_sync": item['is_allow_sync'],
"is_restricted": item['is_restricted'],
"do_notify": item['do_notify']
}
return user_details
else:
# If there is no user data we must return something
# Use "Local" user to retain compatibility with PlexWatch database value
return {"user_id": None,
"username": 'Local',
"friendly_name": 'Local',
"email": '',
"thumb": '',
"is_home_user": 0,
"is_allow_sync": 0,
"is_restricted": 0,
"do_notify": 0
}
def get_home_stats(self, time_range='30'):
def get_home_stats(self, time_range='30', stat_type='0', stat_count='5'):
monitor_db = database.MonitorDatabase()
if not time_range.isdigit():
time_range = '30'
stats_queries = ["top_tv", "popular_tv", "top_users", "top_platforms"]
sort_type = 'total_plays' if stat_type == '0' else 'total_duration'
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", "last_watched"]
home_stats = []
for stat in stats_queries:
@@ -449,6 +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,' \
'session_history_metadata.grandparent_rating_key, ' \
'MAX(session_history.started) as last_watch,' \
'session_history_metadata.grandparent_thumb ' \
@@ -458,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 total_plays DESC LIMIT 10' % 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.")
@@ -467,10 +175,11 @@ class DataFactory(object):
for item in result:
row = {'title': item[1],
'total_plays': item[2],
'total_duration': item[3],
'users_watched': '',
'rating_key': item[3],
'last_play': item[4],
'grandparent_thumb': item[5],
'rating_key': item[4],
'last_play': item[5],
'grandparent_thumb': item[6],
'thumb': '',
'user': '',
'friendly_name': '',
@@ -481,8 +190,55 @@ class DataFactory(object):
top_tv.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_tv})
elif 'top_movies' in stat:
top_movies = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \
'COUNT(session_history_metadata.full_title) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch,' \
'session_history_metadata.thumb ' \
'FROM session_history_metadata ' \
'JOIN session_history on session_history_metadata.id = session_history.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, 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],
'total_plays': item[2],
'total_duration': item[3],
'users_watched': '',
'rating_key': item[4],
'last_play': item[5],
'grandparent_thumb': '',
'thumb': item[6],
'user': '',
'friendly_name': '',
'platform_type': '',
'platform': '',
'row_id': item[0]
}
top_movies.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_movies})
elif 'popular_tv' in stat:
popular_tv = []
try:
@@ -500,7 +256,7 @@ class DataFactory(object):
'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
'LIMIT %s' % (time_range, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
@@ -525,6 +281,48 @@ class DataFactory(object):
home_stats.append({'stat_id': stat,
'rows': popular_tv})
elif 'popular_movies' in stat:
popular_movies = []
try:
query = 'SELECT session_history_metadata.id, ' \
'session_history_metadata.full_title, ' \
'COUNT(DISTINCT session_history.user_id) as users_watched, ' \
'session_history_metadata.rating_key, ' \
'MAX(session_history.started) as last_watch, ' \
'COUNT(session_history.id) as total_plays, ' \
'session_history_metadata.thumb ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") ' \
'>= datetime("now", "-%s days", "localtime") ' \
'AND session_history_metadata.media_type = "movie" ' \
'GROUP BY session_history_metadata.full_title ' \
'ORDER BY users_watched DESC, total_plays DESC ' \
'LIMIT %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': '',
'thumb': item[6],
'user': '',
'friendly_name': '',
'platform_type': '',
'platform': '',
'row_id': item[0]
}
popular_movies.append(row)
home_stats.append({'stat_id': stat,
'rows': popular_movies})
elif 'top_users' in stat:
top_users = []
try:
@@ -532,8 +330,12 @@ class DataFactory(object):
'(case when users.friendly_name is null then session_history.user else ' \
'users.friendly_name end) as friendly_name,' \
'COUNT(session_history.id) as total_plays, ' \
'cast(round(SUM(round((julianday(datetime(session_history.stopped, "unixepoch", "localtime")) - ' \
'julianday(datetime(session_history.started, "unixepoch", "localtime"))) * 86400) - ' \
'(CASE WHEN session_history.paused_counter IS NULL THEN 0 ' \
'ELSE session_history.paused_counter END))/60) as integer) as total_duration,' \
'MAX(session_history.started) as last_watch, ' \
'users.thumb, ' \
'users.custom_avatar_url as thumb, ' \
'users.user_id ' \
'FROM session_history ' \
'JOIN session_history_metadata ON session_history.id = session_history_metadata.id ' \
@@ -541,24 +343,25 @@ class DataFactory(object):
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-%s days", "localtime") '\
'GROUP BY session_history.user_id ' \
'ORDER BY total_plays DESC LIMIT 10' % time_range
'ORDER BY %s DESC LIMIT %s' % (time_range, sort_type, stat_count)
result = monitor_db.select(query)
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
if not item[4] or item[4] == '':
if not item[5] or item[5] == '':
user_thumb = common.DEFAULT_USER_THUMB
else:
user_thumb = item[4]
user_thumb = item[5]
row = {'user': item[0],
'user_id': item[5],
'user_id': item[6],
'friendly_name': item[1],
'total_plays': item[2],
'last_play': item[3],
'thumb': user_thumb,
'total_duration': item[3],
'last_play': item[4],
'user_thumb': user_thumb,
'grandparent_thumb': '',
'users_watched': '',
'rating_key': '',
@@ -570,6 +373,7 @@ class DataFactory(object):
top_users.append(row)
home_stats.append({'stat_id': stat,
'stat_type': sort_type,
'rows': top_users})
elif 'top_platforms' in stat:
@@ -578,12 +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,' \
'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.")
@@ -592,7 +400,8 @@ class DataFactory(object):
for item in result:
row = {'platform': item[0],
'total_plays': item[1],
'last_play': item[2],
'total_duration': item[2],
'last_play': item[3],
'platform_type': item[0],
'title': '',
'thumb': '',
@@ -606,8 +415,62 @@ class DataFactory(object):
top_platform.append(row)
home_stats.append({'stat_id': stat,
'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):
@@ -625,7 +488,6 @@ class DataFactory(object):
else:
return None
print result
stream_output = {}
for item in result:
@@ -663,21 +525,21 @@ class DataFactory(object):
try:
if user_id:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user_id = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user_id, limit])
elif user:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata ' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'WHERE user = ? AND session_history.media_type != "track" ORDER BY started DESC LIMIT ?'
result = monitor_db.select(query, args=[user, limit])
else:
query = 'SELECT session_history.id, session_history.media_type, session_history.rating_key, title, ' \
'thumb, parent_thumb, media_index, parent_media_index, year, started, user ' \
'grandparent_title, thumb, parent_thumb, grandparent_thumb, media_index, parent_media_index, year, started, user ' \
'FROM session_history_metadata WHERE session_history.media_type != "track"' \
'JOIN session_history ON session_history_metadata.id = session_history.id ' \
'ORDER BY started DESC LIMIT ?'
@@ -687,119 +549,37 @@ class DataFactory(object):
return None
for row in result:
if row[1] == 'episode':
thumb = row[5]
if row[1] == 'episode' and row[6]:
thumb = row[6]
elif row[1] == 'episode':
thumb = row[7]
else:
thumb = row[4]
thumb = row[5]
recent_output = {'row_id': row[0],
'type': row[1],
'rating_key': row[2],
'title': row[3],
'parent_title': row[4],
'thumb': thumb,
'index': row[6],
'parentIndex': row[7],
'year': row[8],
'time': row[9],
'user': row[10]
'index': row[8],
'parent_index': row[9],
'year': row[10],
'time': row[11],
'user': row[12]
}
recently_watched.append(recent_output)
return recently_watched
def get_user_watch_time_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase()
time_queries = [1, 7, 30, 0]
user_watch_time_stats = []
for days in time_queries:
if days > 0:
if user_id:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user_id = ?' % days
result = monitor_db.select(query, args=[user_id])
elif user:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'AND user = ?' % days
result = monitor_db.select(query, args=[user])
else:
query = 'SELECT (SUM(stopped - started) - ' \
'SUM(CASE WHEN paused_counter is null THEN 0 ELSE paused_counter END)) as total_time, ' \
'COUNT(id) AS total_plays ' \
'FROM session_history ' \
'WHERE user = ?'
result = monitor_db.select(query, args=[user])
for item in result:
if item[0]:
total_time = item[0]
total_plays = item[1]
else:
total_time = 0
total_plays = 0
row = {'query_days': days,
'total_time': total_time,
'total_plays': total_plays
}
user_watch_time_stats.append(row)
return user_watch_time_stats
def get_user_platform_stats(self, user=None, user_id=None):
monitor_db = database.MonitorDatabase()
platform_stats = []
result_id = 0
try:
if user_id:
query = 'SELECT player, COUNT(player) as player_count, platform ' \
'FROM session_history ' \
'WHERE user_id = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user_id])
else:
query = 'SELECT player, COUNT(player) as player_count, platform ' \
'FROM session_history ' \
'WHERE user = ? ' \
'GROUP BY player ' \
'ORDER BY player_count DESC'
result = monitor_db.select(query, args=[user])
except:
logger.warn("Unable to execute database query.")
return None
for item in result:
row = {'platform_name': item[0],
'platform_type': item[2],
'total_plays': item[1],
'result_id': result_id
}
platform_stats.append(row)
result_id += 1
return platform_stats
def get_metadata_details(self, row_id):
monitor_db = database.MonitorDatabase()
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])
@@ -808,13 +588,15 @@ class DataFactory(object):
metadata = {}
for item in result:
directors = item['directors'].split(';')
writers = item['writers'].split(';')
actors = item['actors'].split(';')
genres = item['genres'].split(';')
directors = item['directors'].split(';') if item['directors'] else []
writers = item['writers'].split(';') if item['writers'] else []
actors = item['actors'].split(';') if item['actors'] else []
genres = item['genres'].split(';') if item['genres'] else []
metadata = {'type': item['media_type'],
'rating_key': item['rating_key'],
'parent_rating_key': item['parent_rating_key'],
'grandparent_rating_key': item['grandparent_rating_key'],
'grandparent_title': item['grandparent_title'],
'parent_index': item['parent_media_index'],
'parent_title': item['parent_title'],
@@ -823,6 +605,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'],
@@ -841,4 +624,48 @@ class DataFactory(object):
'actors': actors
}
return metadata
return metadata
def delete_session_history_rows(self, row_id=None):
monitor_db = database.MonitorDatabase()
if row_id.isdigit():
logger.info(u"PlexPy DataFactory :: Deleting row id %s from the session history database." % row_id)
session_history_del = \
monitor_db.action('DELETE FROM session_history WHERE id = ?', [row_id])
session_history_media_info_del = \
monitor_db.action('DELETE FROM session_history_media_info WHERE id = ?', [row_id])
session_history_metadata_del = \
monitor_db.action('DELETE FROM session_history_metadata WHERE id = ?', [row_id])
return 'Deleted rows %s.' % row_id
else:
return 'Unable to delete rows. Input row not valid.'
def delete_all_user_history(self, user_id=None):
monitor_db = database.MonitorDatabase()
if user_id.isdigit():
logger.info(u"PlexPy DataFactory :: Deleting all history for user id %s from database." % user_id)
session_history_media_info_del = \
monitor_db.action('DELETE FROM '
'session_history_media_info '
'WHERE session_history_media_info.id IN (SELECT session_history_media_info.id '
'FROM session_history_media_info '
'JOIN session_history ON session_history_media_info.id = session_history.id '
'WHERE session_history.user_id = ?)', [user_id])
session_history_metadata_del = \
monitor_db.action('DELETE FROM '
'session_history_metadata '
'WHERE session_history_metadata.id IN (SELECT session_history_metadata.id '
'FROM session_history_metadata '
'JOIN session_history ON session_history_metadata.id = session_history.id '
'WHERE session_history.user_id = ?)', [user_id])
session_history_del = \
monitor_db.action('DELETE FROM '
'session_history '
'WHERE session_history.user_id = ?', [user_id])
return 'Deleted all items for user_id %s.' % user_id
else:
return 'Unable to delete items. Input user_id not valid.'

View File

@@ -259,7 +259,7 @@ class Graphs(object):
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
for item in result:

View File

@@ -18,16 +18,18 @@ from plexpy import logger, helpers
from httplib import HTTPSConnection
from httplib import HTTPConnection
import ssl
class HTTPHandler(object):
"""
Retrieve data from Plex Server
"""
def __init__(self, host, port, token):
def __init__(self, host, port, token, ssl_verify=True):
self.host = host
self.port = str(port)
self.token = token
self.ssl_verify = ssl_verify
"""
Handle the HTTP requests.
@@ -50,9 +52,14 @@ class HTTPHandler(object):
if uri:
if proto.upper() == 'HTTPS':
handler = HTTPSConnection(self.host, self.port, timeout=10)
if not self.ssl_verify and hasattr(ssl, '_create_unverified_context'):
context = ssl._create_unverified_context()
handler = HTTPSConnection(host=self.host, port=self.port, timeout=10, context=context)
logger.warn(u"PlexPy HTTP Handler :: Unverified HTTPS request made. This connection is not secure.")
else:
handler = HTTPSConnection(host=self.host, port=self.port, timeout=10)
else:
handler = HTTPConnection(self.host, self.port, timeout=10)
handler = HTTPConnection(host=self.host, port=self.port, timeout=10)
token_string = ''
if not no_token:

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
@@ -59,6 +59,11 @@ def check_active_sessions():
# Push it on it's own thread so we don't hold up our db actions
threading.Thread(target=notification_handler.notify,
kwargs=dict(stream_data=stream, notify_action='pause')).start()
if session['state'] == 'playing' and stream['state'] == 'paused':
# Push any notifications -
# Push it on it's own thread so we don't hold up our db actions
threading.Thread(target=notification_handler.notify,
kwargs=dict(stream_data=stream, notify_action='resume')).start()
if stream['state'] == 'paused':
# The stream is still paused so we need to increment the paused_counter
# Using the set config parameter as the interval, probably not the most accurate but
@@ -67,6 +72,53 @@ def check_active_sessions():
monitor_db.action('UPDATE sessions SET paused_counter = ? '
'WHERE session_key = ? AND rating_key = ?',
[paused_counter, stream['session_key'], stream['rating_key']])
if session['state'] == 'buffering' and plexpy.CONFIG.BUFFER_THRESHOLD > 0:
# The stream is buffering so we need to increment the buffer_count
# We're going just increment on every monitor ping,
# would be difficult to keep track otherwise
monitor_db.action('UPDATE sessions SET buffer_count = buffer_count + 1 '
'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
# Check the current buffer count and last buffer to determine if we should notify
buffer_values = monitor_db.select('SELECT buffer_count, buffer_last_triggered '
'FROM sessions '
'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
if buffer_values[0]['buffer_count'] >= plexpy.CONFIG.BUFFER_THRESHOLD:
# Push any notifications -
# Push it on it's own thread so we don't hold up our db actions
# Our first buffer notification
if buffer_values[0]['buffer_count'] == plexpy.CONFIG.BUFFER_THRESHOLD:
logger.info(u"PlexPy Monitor :: User '%s' has triggered a buffer warning."
% stream['user'])
# Set the buffer trigger time
monitor_db.action('UPDATE sessions '
'SET buffer_last_triggered = strftime("%s","now") '
'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
threading.Thread(target=notification_handler.notify,
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
else:
# Subsequent buffer notifications after wait time
if int(time.time()) > buffer_values[0]['buffer_last_triggered'] + \
plexpy.CONFIG.BUFFER_WAIT:
logger.info(u"PlexPy Monitor :: User '%s' has triggered multiple buffer warnings."
% stream['user'])
# Set the buffer trigger time
monitor_db.action('UPDATE sessions '
'SET buffer_last_triggered = strftime("%s","now") '
'WHERE session_key = ? AND rating_key = ?',
[stream['session_key'], stream['rating_key']])
threading.Thread(target=notification_handler.notify,
kwargs=dict(stream_data=stream, notify_action='buffer')).start()
logger.debug(u"PlexPy Monitor :: Stream buffering. Count is now %s. Last triggered %s."
% (buffer_values[0][0], buffer_values[0][1]))
# Check if the user has reached the offset in the media we defined as the "watched" percent
# Don't trigger if state is buffer as some clients push the progress to the end when
# buffering on start.
@@ -179,6 +231,10 @@ class MonitorProcessing(object):
self.db.upsert('sessions', timestamp, keys)
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
from plexpy import users
user_data = users.Users()
user_details = user_data.get_user_friendly_name(user=session['user'])
if session:
logging_enabled = False
@@ -218,6 +274,10 @@ class MonitorProcessing(object):
(session['rating_key'], str(int(stopped) - session['started']),
import_ignore_interval))
if not user_details['keep_history'] and not is_import:
logging_enabled = False
logger.debug(u"PlexPy Monitor :: History logging for user '%s' is disabled." % session['user'])
if logging_enabled:
# logger.debug(u"PlexPy Monitor :: Attempting to write to session_history table...")
query = 'INSERT INTO session_history (started, stopped, rating_key, parent_rating_key, ' \
@@ -283,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

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -20,12 +20,12 @@ import time
def notify(stream_data=None, notify_action=None):
from plexpy import datafactory
from plexpy import users
if stream_data and notify_action:
# Check if notifications enabled for user
data_factory = datafactory.DataFactory()
user_details = data_factory.get_user_friendly_name(user=stream_data['user'])
user_data = users.Users()
user_details = user_data.get_user_friendly_name(user=stream_data['user'])
if not user_details['do_notify']:
return
@@ -52,6 +52,33 @@ def notify(stream_data=None, notify_action=None):
set_notify_state(session=stream_data, state='stop', agent_info=agent)
elif agent['on_pause'] and notify_action == 'pause':
# Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
set_notify_state(session=stream_data, state='pause', agent_info=agent)
elif agent['on_resume'] and notify_action == 'resume':
# Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
set_notify_state(session=stream_data, state='resume', agent_info=agent)
elif agent['on_buffer'] and notify_action == 'buffer':
# Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
set_notify_state(session=stream_data, state='buffer', agent_info=agent)
elif agent['on_watched'] and notify_action == 'watched':
# Get the current states for notifications from our db
notify_states = get_notify_state(session=stream_data)
@@ -100,6 +127,33 @@ def notify(stream_data=None, notify_action=None):
# Set the notification state in the db
set_notify_state(session=stream_data, state='stop', agent_info=agent)
elif agent['on_pause'] and notify_action == 'pause':
# Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='pause', agent_info=agent)
elif agent['on_resume'] and notify_action == 'resume':
# Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='resume', agent_info=agent)
elif agent['on_buffer'] and notify_action == 'buffer':
# Build and send notification
notify_strings = build_notify_text(session=stream_data, state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
# Set the notification state in the db
set_notify_state(session=stream_data, state='buffer', agent_info=agent)
elif stream_data['media_type'] == 'clip':
pass
else:
@@ -108,9 +162,10 @@ 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_watched, agent_id '
result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id '
'FROM notify_log '
'WHERE session_key = ? '
'AND rating_key = ? '
@@ -121,12 +176,16 @@ def get_notify_state(session):
for item in result:
notify_state = {'on_play': item[0],
'on_stop': item[1],
'on_watched': item[2],
'agent_id': item[3]}
'on_pause': item[2],
'on_resume': item[3],
'on_buffer': item[4],
'on_watched': item[5],
'agent_id': item[6]}
notify_states.append(notify_state)
return notify_states
def set_notify_state(session, state, agent_info):
if session and state and agent_info:
@@ -136,6 +195,12 @@ def set_notify_state(session, state, agent_info):
values = {'on_play': int(time.time())}
elif state == 'stop':
values = {'on_stop': int(time.time())}
elif state == 'pause':
values = {'on_pause': int(time.time())}
elif state == 'resume':
values = {'on_resume': int(time.time())}
elif state == 'buffer':
values = {'on_buffer': int(time.time())}
elif state == 'watched':
values = {'on_watched': int(time.time())}
else:
@@ -152,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
@@ -173,34 +239,28 @@ def build_notify_text(session, state):
if session['media_type'] == 'episode':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<movie>[^>]+.</movie>|<music>[^>]+.</music>', re.IGNORECASE)
# Remove the unwanted tags and strip any unmatch tags too.
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
elif session['media_type'] == 'movie':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<tv>[^>]+.</tv>|<music>[^>]+.</music>', re.IGNORECASE)
# Remove the unwanted tags and strip any unmatch tags too.
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
elif session['media_type'] == 'track':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<tv>[^>]+.</tv>|<movie>[^>]+.</movie>', re.IGNORECASE)
else:
pattern = None
if session['media_type'] == 'episode' or session['media_type'] == 'movie' or session['media_type'] == 'track' \
and pattern:
# Remove the unwanted tags and strip any unmatch tags too.
on_start_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT))
on_start_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT))
on_stop_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT))
on_stop_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT))
on_pause_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT))
on_pause_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT))
on_resume_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT))
on_resume_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT))
on_buffer_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT))
on_buffer_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT))
on_watched_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT))
on_watched_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT))
else:
@@ -208,6 +268,12 @@ def build_notify_text(session, state):
on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT
on_stop_subject = plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT
on_stop_body = plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT
on_pause_subject = plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT
on_pause_body = plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT
on_resume_subject = plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT
on_resume_body = plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT
on_buffer_subject = plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT
on_buffer_body = plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT
on_watched_subject = plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT
on_watched_body = plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT
@@ -237,6 +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':
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)
@@ -254,10 +327,13 @@ def build_notify_text(session, state):
'content_rating': item_metadata['content_rating'],
'summary': item_metadata['summary'],
'season_num': item_metadata['parent_index'],
'season_num00': item_metadata['parent_index'].zfill(2),
'episode_num': item_metadata['index'],
'episode_num00': item_metadata['index'].zfill(2),
'album_name': item_metadata['parent_title'],
'rating': item_metadata['rating'],
'duration': duration,
'stream_duration': stream_duration,
'progress': view_offset,
'progress_percent': progress_percent
}
@@ -273,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:
@@ -297,14 +373,86 @@ 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:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text]
else:
return [subject_text, body_text]
elif state == 'pause':
# Default body text
body_text = '%s (%s) has paused %s' % (session['friendly_name'],
session['player'],
full_title)
if on_pause_subject and on_pause_body:
try:
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 = 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:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text]
else:
return [subject_text, body_text]
elif state == 'resume':
# Default body text
body_text = '%s (%s) has resumed %s' % (session['friendly_name'],
session['player'],
full_title)
if on_resume_subject and on_resume_body:
try:
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 = 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:
logger.error(u"PlexPy Notifier :: Unable to parse custom notification body. Using fallback.")
return [subject_text, body_text]
else:
return [subject_text, body_text]
elif state == 'buffer':
# Default body text
body_text = '%s (%s) is buffering %s' % (session['friendly_name'],
session['player'],
full_title)
if on_buffer_subject and on_buffer_body:
try:
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 = 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:
@@ -321,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:
@@ -340,6 +488,7 @@ def build_notify_text(session, state):
else:
return None
def strip_tag(data):
import re

View File

@@ -59,6 +59,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.GROWL_ENABLED),
'on_play': plexpy.CONFIG.GROWL_ON_PLAY,
'on_stop': plexpy.CONFIG.GROWL_ON_STOP,
'on_pause': plexpy.CONFIG.GROWL_ON_PAUSE,
'on_resume': plexpy.CONFIG.GROWL_ON_RESUME,
'on_buffer': plexpy.CONFIG.GROWL_ON_BUFFER,
'on_watched': plexpy.CONFIG.GROWL_ON_WATCHED
},
{'name': 'Prowl',
@@ -68,6 +71,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.PROWL_ENABLED),
'on_play': plexpy.CONFIG.PROWL_ON_PLAY,
'on_stop': plexpy.CONFIG.PROWL_ON_STOP,
'on_pause': plexpy.CONFIG.PROWL_ON_PAUSE,
'on_resume': plexpy.CONFIG.PROWL_ON_RESUME,
'on_buffer': plexpy.CONFIG.PROWL_ON_BUFFER,
'on_watched': plexpy.CONFIG.PROWL_ON_WATCHED
},
{'name': 'XBMC',
@@ -77,6 +83,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.XBMC_ENABLED),
'on_play': plexpy.CONFIG.XBMC_ON_PLAY,
'on_stop': plexpy.CONFIG.XBMC_ON_STOP,
'on_pause': plexpy.CONFIG.XBMC_ON_PAUSE,
'on_resume': plexpy.CONFIG.XBMC_ON_RESUME,
'on_buffer': plexpy.CONFIG.XBMC_ON_BUFFER,
'on_watched': plexpy.CONFIG.XBMC_ON_WATCHED
},
{'name': 'Plex',
@@ -86,6 +95,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.PLEX_ENABLED),
'on_play': plexpy.CONFIG.PLEX_ON_PLAY,
'on_stop': plexpy.CONFIG.PLEX_ON_STOP,
'on_pause': plexpy.CONFIG.PLEX_ON_PAUSE,
'on_resume': plexpy.CONFIG.PLEX_ON_RESUME,
'on_buffer': plexpy.CONFIG.PLEX_ON_BUFFER,
'on_watched': plexpy.CONFIG.PLEX_ON_WATCHED
},
{'name': 'NotifyMyAndroid',
@@ -95,6 +107,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.NMA_ENABLED),
'on_play': plexpy.CONFIG.NMA_ON_PLAY,
'on_stop': plexpy.CONFIG.NMA_ON_STOP,
'on_pause': plexpy.CONFIG.NMA_ON_PAUSE,
'on_resume': plexpy.CONFIG.NMA_ON_RESUME,
'on_buffer': plexpy.CONFIG.NMA_ON_BUFFER,
'on_watched': plexpy.CONFIG.NMA_ON_WATCHED
},
{'name': 'Pushalot',
@@ -104,6 +119,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.PUSHALOT_ENABLED),
'on_play': plexpy.CONFIG.PUSHALOT_ON_PLAY,
'on_stop': plexpy.CONFIG.PUSHALOT_ON_STOP,
'on_pause': plexpy.CONFIG.PUSHALOT_ON_PAUSE,
'on_resume': plexpy.CONFIG.PUSHALOT_ON_RESUME,
'on_buffer': plexpy.CONFIG.PUSHALOT_ON_BUFFER,
'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED
},
{'name': 'Pushbullet',
@@ -113,6 +131,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.PUSHBULLET_ENABLED),
'on_play': plexpy.CONFIG.PUSHBULLET_ON_PLAY,
'on_stop': plexpy.CONFIG.PUSHBULLET_ON_STOP,
'on_pause': plexpy.CONFIG.PUSHBULLET_ON_PAUSE,
'on_resume': plexpy.CONFIG.PUSHBULLET_ON_RESUME,
'on_buffer': plexpy.CONFIG.PUSHBULLET_ON_BUFFER,
'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED
},
{'name': 'Pushover',
@@ -122,6 +143,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.PUSHOVER_ENABLED),
'on_play': plexpy.CONFIG.PUSHOVER_ON_PLAY,
'on_stop': plexpy.CONFIG.PUSHOVER_ON_STOP,
'on_pause': plexpy.CONFIG.PUSHOVER_ON_PAUSE,
'on_resume': plexpy.CONFIG.PUSHOVER_ON_RESUME,
'on_buffer': plexpy.CONFIG.PUSHOVER_ON_BUFFER,
'on_watched': plexpy.CONFIG.PUSHOVER_ON_WATCHED
},
{'name': 'Boxcar2',
@@ -131,6 +155,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.BOXCAR_ENABLED),
'on_play': plexpy.CONFIG.BOXCAR_ON_PLAY,
'on_stop': plexpy.CONFIG.BOXCAR_ON_STOP,
'on_pause': plexpy.CONFIG.BOXCAR_ON_PAUSE,
'on_resume': plexpy.CONFIG.BOXCAR_ON_RESUME,
'on_buffer': plexpy.CONFIG.BOXCAR_ON_BUFFER,
'on_watched': plexpy.CONFIG.BOXCAR_ON_WATCHED
},
{'name': 'E-mail',
@@ -140,6 +167,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.EMAIL_ENABLED),
'on_play': plexpy.CONFIG.EMAIL_ON_PLAY,
'on_stop': plexpy.CONFIG.EMAIL_ON_STOP,
'on_pause': plexpy.CONFIG.EMAIL_ON_PAUSE,
'on_resume': plexpy.CONFIG.EMAIL_ON_RESUME,
'on_buffer': plexpy.CONFIG.EMAIL_ON_BUFFER,
'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED
}
]
@@ -154,6 +184,9 @@ def available_notification_agents():
'state': checked(plexpy.CONFIG.OSX_NOTIFY_ENABLED),
'on_play': plexpy.CONFIG.OSX_NOTIFY_ON_PLAY,
'on_stop': plexpy.CONFIG.OSX_NOTIFY_ON_STOP,
'on_pause': plexpy.CONFIG.OSX_NOTIFY_ON_PAUSE,
'on_resume': plexpy.CONFIG.OSX_NOTIFY_ON_RESUME,
'on_buffer': plexpy.CONFIG.OSX_NOTIFY_ON_BUFFER,
'on_watched': plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED
})

View File

@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers, datafactory, http_handler, database
from plexpy import logger, helpers, users, http_handler, database
from xml.dom import minidom
@@ -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'],
@@ -37,6 +36,18 @@ def refresh_users():
"is_restricted": item['is_restricted']
}
# Check if we've set a custom avatar if so don't overwrite it.
if item['user_id']:
avatar_urls = monitor_db.select('SELECT thumb, custom_avatar_url '
'FROM users WHERE user_id = ?',
[item['user_id']])
if avatar_urls:
if not avatar_urls[0]['custom_avatar_url'] or \
avatar_urls[0]['custom_avatar_url'] == avatar_urls[0]['thumb']:
new_value_dict['custom_avatar_url'] = item['thumb']
else:
new_value_dict['custom_avatar_url'] = item['thumb']
monitor_db.upsert('users', new_value_dict, control_value_dict)
logger.info("Users list refreshed.")
@@ -89,10 +100,12 @@ class PlexTV(object):
self.protocol = 'HTTPS'
self.username = username
self.password = password
self.ssl_verify = plexpy.CONFIG.VERIFY_SSL_CERT
self.request_handler = http_handler.HTTPHandler(host='plex.tv',
port=443,
token=plexpy.CONFIG.PMS_TOKEN)
token=plexpy.CONFIG.PMS_TOKEN,
ssl_verify=self.ssl_verify)
def get_plex_auth(self, output_format='raw'):
uri = '/users/sign_in.xml'
@@ -242,7 +255,7 @@ class PlexTV(object):
def get_synced_items(self, machine_id=None, user_id=None):
sync_list = self.get_plextv_sync_lists(machine_id)
data_factory = datafactory.DataFactory()
user_data = users.Users()
synced_items = []
@@ -266,8 +279,8 @@ class PlexTV(object):
for device in sync_device:
device_user_id = helpers.get_xml_attr(device, 'userID')
try:
device_username = data_factory.get_user_details(user_id=device_user_id)['username']
device_friendly_name = data_factory.get_user_details(user_id=device_user_id)['friendly_name']
device_username = user_data.get_user_details(user_id=device_user_id)['username']
device_friendly_name = user_data.get_user_details(user_id=device_user_id)['friendly_name']
except:
device_username = ''
device_friendly_name = ''

View File

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

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
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, helpers, datafactory, http_handler
from plexpy import logger, helpers, users, http_handler
from urlparse import urlparse
import plexpy
@@ -89,6 +89,23 @@ class PmsConnect(object):
return request
"""
Return list of seasons in requested show.
Parameters required: rating_key { ratingKey of parent }
Optional parameters: output_format { dict, json }
Output: array
"""
def get_season_list(self, rating_key='', output_format=''):
uri = '/library/metadata/' + rating_key + '/children'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
return request
"""
Return list of episodes in requested season.
@@ -103,7 +120,7 @@ class PmsConnect(object):
proto=self.protocol,
request_type='GET',
output_format=output_format)
return request
"""
@@ -154,6 +171,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.
@@ -218,6 +267,7 @@ class PmsConnect(object):
recent_items = {'type': recent_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'thumb': helpers.get_xml_attr(item, 'thumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt')
}
@@ -232,6 +282,7 @@ class PmsConnect(object):
recent_items = {'type': recent_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'year': helpers.get_xml_attr(item, 'year'),
'thumb': helpers.get_xml_attr(item, 'thumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt')
@@ -311,6 +362,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'),
@@ -332,6 +384,8 @@ class PmsConnect(object):
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'),
@@ -340,6 +394,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'),
@@ -369,6 +424,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'),
@@ -392,6 +448,7 @@ class PmsConnect(object):
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'),
@@ -400,6 +457,7 @@ class PmsConnect(object):
'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'),
@@ -429,6 +487,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'),
@@ -505,7 +564,7 @@ class PmsConnect(object):
"""
def get_session_each(self, stream_type='', session=None):
session_output = None
data_factory = datafactory.DataFactory()
user_data = users.Users()
if stream_type == 'track':
media_info = session.getElementsByTagName('Media')[0]
@@ -519,6 +578,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')
@@ -526,12 +588,15 @@ 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 = ''
transcode_protocol = ''
user_details = data_factory.get_user_details(
user_details = user_data.get_user_details(
user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title'))
if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Track'):
@@ -550,6 +615,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,
@@ -560,6 +626,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,
@@ -604,6 +673,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')
@@ -614,6 +686,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 = ''
@@ -639,7 +714,7 @@ class PmsConnect(object):
else:
use_indexes = 0
user_details = data_factory.get_user_details(
user_details = user_data.get_user_details(
user=helpers.get_xml_attr(session.getElementsByTagName('User')[0], 'title'))
if helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'machineIdentifier').endswith('_Video'):
@@ -659,6 +734,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,
@@ -666,9 +742,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,
@@ -691,9 +771,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'),
@@ -706,6 +790,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,
@@ -713,9 +798,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,
@@ -738,9 +827,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'),
@@ -753,6 +846,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,
@@ -760,9 +854,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,
@@ -793,6 +891,49 @@ class PmsConnect(object):
return session_output
"""
Return processed and validated season list.
Output: array
"""
def get_show_children(self, rating_key=''):
season_data = self.get_season_list(rating_key, output_format='xml')
try:
xml_head = season_data.getElementsByTagName('MediaContainer')
except:
logger.warn("Unable to parse XML for get_season_list.")
return []
season_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
logger.debug(u"No season data.")
season_list = {'season_count': '0',
'season_list': []
}
return season_list
if a.getElementsByTagName('Directory'):
result_data = a.getElementsByTagName('Directory')
for result in result_data:
season_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'),
'parent_thumb': helpers.get_xml_attr(a, 'thumb')
}
season_list.append(season_output)
output = {'season_count': helpers.get_xml_attr(xml_head[0], 'size'),
'title': helpers.get_xml_attr(xml_head[0], 'title2'),
'season_list': season_list
}
return output
"""
Return processed and validated episode list.
@@ -912,6 +1053,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

527
plexpy/users.py Normal file
View File

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

View File

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

View File

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

View File

@@ -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
@@ -13,7 +13,7 @@
# You should have received a copy of the GNU General Public License
# along with PlexPy. If not, see <http://www.gnu.org/licenses/>.
from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs
from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs, users
from plexpy.helpers import checked, radio
from mako.lookup import TemplateLookup
@@ -64,7 +64,13 @@ class WebInterface(object):
@cherrypy.expose
def home(self):
return serve_template(templatename="index.html", title="Home")
config = {
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"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)
@cherrypy.expose
def welcome(self, **kwargs):
@@ -115,12 +121,19 @@ class WebInterface(object):
return json.dumps(formats)
@cherrypy.expose
def home_stats(self, time_range='30', **kwargs):
def home_stats(self, time_range='30', stat_type='0', stat_count='5', **kwargs):
data_factory = datafactory.DataFactory()
stats_data = data_factory.get_home_stats(time_range=time_range)
stats_data = data_factory.get_home_stats(time_range=time_range, stat_type=stat_type, 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")
@@ -139,16 +152,15 @@ class WebInterface(object):
@cherrypy.expose
def user(self, user=None, user_id=None):
user_data = users.Users()
if user_id:
try:
data_factory = datafactory.DataFactory()
user_details = data_factory.get_user_details(user_id=user_id)
user_details = user_data.get_user_details(user_id=user_id)
except:
logger.warn("Unable to retrieve friendly name for user_id %s " % user_id)
elif user:
try:
data_factory = datafactory.DataFactory()
user_details = data_factory.get_user_details(user=user)
user_details = user_data.get_user_details(user=user)
except:
logger.warn("Unable to retrieve friendly name for user %s " % user)
else:
@@ -159,13 +171,12 @@ class WebInterface(object):
@cherrypy.expose
def edit_user_dialog(self, user=None, user_id=None, **kwargs):
user_data = users.Users()
if user_id:
data_factory = datafactory.DataFactory()
result = data_factory.get_user_friendly_name(user_id=user_id)
result = user_data.get_user_friendly_name(user_id=user_id)
status_message = ''
elif user:
data_factory = datafactory.DataFactory()
result = data_factory.get_user_friendly_name(user=user)
result = user_data.get_user_friendly_name(user=user)
status_message = ''
else:
result = None
@@ -179,10 +190,24 @@ class WebInterface(object):
do_notify = kwargs.get('do_notify')
else:
do_notify = 0
if 'keep_history' in kwargs:
keep_history = kwargs.get('keep_history')
else:
keep_history = 0
if 'thumb' in kwargs:
custom_avatar = kwargs['thumb']
else:
custom_avatar = ''
user_data = users.Users()
if user_id:
try:
data_factory = datafactory.DataFactory()
data_factory.set_user_friendly_name(user_id=user_id, friendly_name=friendly_name, do_notify=do_notify)
user_data.set_user_friendly_name(user_id=user_id,
friendly_name=friendly_name,
do_notify=do_notify,
keep_history=keep_history)
user_data.set_user_profile_url(user_id=user_id,
profile_url=custom_avatar)
status_message = "Successfully updated user."
return status_message
@@ -191,8 +216,12 @@ class WebInterface(object):
return status_message
if user:
try:
data_factory = datafactory.DataFactory()
data_factory.set_user_friendly_name(user=user, friendly_name=friendly_name, do_notify=do_notify)
user_data.set_user_friendly_name(user=user,
friendly_name=friendly_name,
do_notify=do_notify,
keep_history=keep_history)
user_data.set_user_profile_url(user=user,
profile_url=custom_avatar)
status_message = "Successfully updated user."
return status_message
@@ -222,11 +251,11 @@ class WebInterface(object):
@cherrypy.expose
def get_user_list(self, **kwargs):
data_factory = datafactory.DataFactory()
users = data_factory.get_user_list(kwargs=kwargs)
user_data = users.Users()
user_list = user_data.get_user_list(kwargs=kwargs)
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps(users)
return json.dumps(user_list)
@cherrypy.expose
def checkGithub(self):
@@ -423,8 +452,19 @@ class WebInterface(object):
"notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT,
"notify_on_stop_subject_text": plexpy.CONFIG.NOTIFY_ON_STOP_SUBJECT_TEXT,
"notify_on_stop_body_text": plexpy.CONFIG.NOTIFY_ON_STOP_BODY_TEXT,
"notify_on_pause_subject_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_SUBJECT_TEXT,
"notify_on_pause_body_text": plexpy.CONFIG.NOTIFY_ON_PAUSE_BODY_TEXT,
"notify_on_resume_subject_text": plexpy.CONFIG.NOTIFY_ON_RESUME_SUBJECT_TEXT,
"notify_on_resume_body_text": plexpy.CONFIG.NOTIFY_ON_RESUME_BODY_TEXT,
"notify_on_buffer_subject_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_SUBJECT_TEXT,
"notify_on_buffer_body_text": plexpy.CONFIG.NOTIFY_ON_BUFFER_BODY_TEXT,
"notify_on_watched_subject_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_SUBJECT_TEXT,
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT
"notify_on_watched_body_text": plexpy.CONFIG.NOTIFY_ON_WATCHED_BODY_TEXT,
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_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
}
return serve_template(templatename="settings.html", title="Settings", config=config)
@@ -445,7 +485,7 @@ class WebInterface(object):
"tv_notify_on_start", "movie_notify_on_start", "music_notify_on_start",
"tv_notify_on_stop", "movie_notify_on_stop", "music_notify_on_stop",
"tv_notify_on_pause", "movie_notify_on_pause", "music_notify_on_pause", "refresh_users_on_startup",
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote"
"ip_logging_enable", "video_logging_enable", "music_logging_enable", "pms_is_remote", "home_stats_type"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
@@ -462,18 +502,34 @@ 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.
threading.Thread(target=plextv.refresh_users).start()
# 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")
@@ -521,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]]
@@ -701,16 +760,19 @@ class WebInterface(object):
@cherrypy.expose
def info(self, item_id=None, source=None, **kwargs):
metadata = None
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:
metadata = result['metadata']
if result:
return serve_template(templatename="info.html", data=result, title="Info")
if metadata:
return serve_template(templatename="info.html", data=metadata, title="Info")
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="info.html", data=None, title="Info")
@@ -732,8 +794,8 @@ class WebInterface(object):
@cherrypy.expose
def get_user_watch_time_stats(self, user=None, user_id=None, **kwargs):
data_factory = datafactory.DataFactory()
result = data_factory.get_user_watch_time_stats(user_id=user_id, user=user)
user_data = users.Users()
result = user_data.get_user_watch_time_stats(user_id=user_id, user=user)
if result:
return serve_template(templatename="user_watch_time_stats.html", data=result, title="Watch Stats")
@@ -744,8 +806,8 @@ class WebInterface(object):
@cherrypy.expose
def get_user_platform_stats(self, user=None, user_id=None, **kwargs):
data_factory = datafactory.DataFactory()
result = data_factory.get_user_platform_stats(user_id=user_id, user=user)
user_data = users.Users()
result = user_data.get_user_platform_stats(user_id=user_id, user=user)
if result:
return serve_template(templatename="user_platform_stats.html", data=result,
@@ -755,7 +817,19 @@ 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_show_children(self, rating_key='', **kwargs):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_show_children(rating_key)
if result:
return serve_template(templatename="info_season_list.html", data=result, title="Season List")
else:
logger.warn('Unable to retrieve data.')
return serve_template(templatename="info_season_list.html", data=None, title="Season List")
@cherrypy.expose
def get_season_children(self, rating_key='', **kwargs):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_season_children(rating_key)
@@ -823,9 +897,9 @@ class WebInterface(object):
elif user:
custom_where = [['user', user]]
data_factory = datafactory.DataFactory()
history = data_factory.get_user_unique_ips(kwargs=kwargs,
custom_where=custom_where)
user_data = users.Users()
history = user_data.get_user_unique_ips(kwargs=kwargs,
custom_where=custom_where)
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps(history)
@@ -1212,3 +1286,48 @@ class WebInterface(object):
return serve_template(templatename="notification_config.html", title="Notification Configuration",
data=config, checkboxes=checkboxes)
@cherrypy.expose
def get_notification_agent_triggers(self, config_id, **kwargs):
if config_id.isdigit():
agents = notifiers.available_notification_agents()
for agent in agents:
if int(config_id) == agent['id']:
this_agent = agent
break
else:
this_agent = None
else:
return None
return serve_template(templatename="notification_triggers_modal.html", title="Notification Triggers",
data=this_agent)
@cherrypy.expose
def delete_history_rows(self, row_id, **kwargs):
data_factory = datafactory.DataFactory()
if row_id:
delete_row = data_factory.delete_session_history_rows(row_id=row_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})
@cherrypy.expose
def delete_all_user_history(self, user_id, **kwargs):
data_factory = datafactory.DataFactory()
if user_id:
delete_row = data_factory.delete_all_user_history(user_id=user_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
else:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': 'no data received'})