Compare commits

...

207 Commits

Author SHA1 Message Date
Jonathan Wong
a00d43092d Merge branch 'dev' 2015-12-20 09:34:38 -08:00
Jonathan Wong
45c2f50018 v1.2.15 2015-12-20 09:33:04 -08:00
JonnyWong16
ef8c6e82e6 Merge pull request #381 from JonnyWong16/miscellaneous-fixes
Group watch statistics history
2015-12-20 03:22:39 -08:00
Jonathan Wong
3eebb58da5 Fix most concurrent count with duplicate time entires 2015-12-20 03:14:39 -08:00
Jonathan Wong
447c50fd03 Group watch statistics history 2015-12-19 20:40:30 -08:00
Jonathan Wong
0620ebebcf Touch up current activity status bar hover effect 2015-12-17 23:26:26 -08:00
JonnyWong16
cf081ee291 Merge pull request #380 from zobe123/patch-1
Added Statusbar hover effect with percentage
2015-12-17 23:18:22 -08:00
zobe123
3c29b8e9c5 Update plexpy.css
Added Statusbar hover effect with percentage
2015-12-17 23:33:53 +01:00
zobe123
6143da5a6a Update pmsconnect.py
some additions to Show readable transcode progress
2015-12-17 23:25:29 +01:00
JonnyWong16
664f71575c Merge pull request #370 from JonnyWong16/miscellaneous-fixes
Miscellaneous fixes
2015-12-16 19:52:07 -08:00
Jonathan Wong
9ae111b8a1 Fix Growl notifications 2015-12-16 19:42:51 -08:00
Jonathan Wong
18682c7a2e Add logger info for Boxcar2 notification sent 2015-12-15 19:56:14 -08:00
Jonathan Wong
b21c50dfcf Fix typo on settings page 2015-12-13 23:53:22 -08:00
Jonathan Wong
49b6965e8e Add CC and BCC and multiple email recipients 2015-12-13 15:08:43 -08:00
Jonathan Wong
c6cc2b8831 Save graph type/days/tab to config file
* Change input for graph days range
2015-12-13 11:35:33 -08:00
Jonathan Wong
f9f65eae53 Add duration to history table footer 2015-12-13 09:36:54 -08:00
Jonathan Wong
b51d442673 Add notification for remote access/server back up 2015-12-13 09:31:11 -08:00
Jonathan Wong
5863b62ccf Add notifier name to modal title 2015-12-12 17:32:32 -08:00
Jonathan Wong
66bb922012 Add stream details to notification options
* Also beautify modal
2015-12-12 17:31:52 -08:00
Jonathan Wong
53876e8f0d Add time range to most concurrent stat
* Reorder cards
2015-12-12 16:21:54 -08:00
Jonathan Wong
cb7ba7fdde Clean up if statement in current activity header 2015-12-12 14:36:57 -08:00
Jonathan Wong
9cf6793b24 Add most concurrent streams home statistic 2015-12-12 14:34:42 -08:00
Jonathan Wong
6e62ffdd22 Get Pushbullet devices automatically using API 2015-12-12 14:34:11 -08:00
Jonathan Wong
c042d9e39a Fix metadata for grouped recently added notifications 2015-12-12 14:31:04 -08:00
Jonathan Wong
1262de2ae2 Clean up notifiers 2015-12-12 14:30:42 -08:00
JonnyWong16
307230cec8 Merge pull request #365 from zobe123/patch-1
Update current_activity_header.html
2015-12-12 14:10:27 -08:00
drzoidberg33
4fa2711e78 Merge pull request #367 from drzoidberg33/frontend-tweaks
Fix greedy search bar.
2015-12-12 16:20:44 +02:00
Tim
5c9c2f9ab8 Fix greedy search bar. 2015-12-12 16:19:30 +02:00
zobe123
604155b41b Update current_activity_header.html
remove "no"
2015-12-11 22:23:14 +01:00
zobe123
6e4198e7be Update current_activity_header.html
dynamic "s" at the word stream/streams
added "no" before Activity when their is no activity
2015-12-10 23:56:02 +01:00
drzoidberg33
14d4940d05 Merge pull request #363 from InAnimaTe/dev
add .pem cert to ignore list
2015-12-09 18:47:22 +02:00
Mario Loria
a6b0cdef97 add .pem cert to ignore list 2015-12-09 11:00:53 -05:00
Tim
6265943607 Merge branch 'dev' 2015-12-07 22:32:00 +02:00
Tim
de39f7691c v1.2.14 2015-12-07 22:31:00 +02:00
drzoidberg33
921a219beb Merge pull request #352 from drzoidberg33/security-fixes
Fix regression on select_single queries Resolves #350
2015-12-07 22:26:33 +02:00
Tim
b9c95d49a6 Fix regression on select_single queries. 2015-12-07 22:22:47 +02:00
Jonathan Wong
fc0be6bce2 Merge branch 'dev' 2015-12-06 11:41:08 -08:00
Jonathan Wong
8db891cfe6 v1.2.13 2015-12-06 11:40:17 -08:00
JonnyWong16
f6e77cc578 Merge pull request #346 from JonnyWong16/miscellaneous-fixes
Fix current activity
2015-12-06 11:36:05 -08:00
Jonathan Wong
a3782f9150 Fix dict key for IP address in current activity 2015-12-06 11:30:33 -08:00
drzoidberg33
7546c7ef42 Merge pull request #345 from drzoidberg33/security-fixes
Security fixes
2015-12-06 21:23:43 +02:00
Jonathan Wong
53de8cda30 Match newline between tags for notification text 2015-12-06 11:13:46 -08:00
Tim
1fb7473dc5 Sanitize sync row items. 2015-12-06 21:04:33 +02:00
Tim
cc9d09bd54 Allow HTML encoded content to be displayed in notification setting descriptions. 2015-12-06 20:49:00 +02:00
Tim
42ff4a2f62 Merge branch 'dev' 2015-12-06 20:02:18 +02:00
Tim
3fa5f80fc4 v1.2.12 2015-12-06 20:01:38 +02:00
drzoidberg33
9b5b7ef8db Merge pull request #343 from drzoidberg33/security-fixes
No need to sanitize same items more than once.
2015-12-06 19:59:34 +02:00
Tim
560acf62fe No need to sanitize same items more than once. 2015-12-06 18:45:02 +02:00
drzoidberg33
27d12922da Merge pull request #339 from drzoidberg33/security-fixes
Move dict_factory out of database class.
2015-12-06 18:10:26 +02:00
Tim
37b92f3d88 Move dict_factory out of database class. 2015-12-06 18:09:18 +02:00
Tim
79d5c0c92e Merge branch 'dev' 2015-12-06 17:33:12 +02:00
Tim
0bdaedd486 Fix more regresssions. 2015-12-06 17:32:25 +02:00
drzoidberg33
018a201688 Merge pull request #338 from drzoidberg33/security-fixes
More dict key fixes.
2015-12-06 17:21:54 +02:00
Tim
a5bd7e6563 More dict key fixes. 2015-12-06 17:19:09 +02:00
drzoidberg33
a055feccd5 Merge pull request #337 from drzoidberg33/security-fixes
Fix changelog modal output.
2015-12-06 16:38:54 +02:00
Tim
ba68a9b52b Fix changelog modal output. 2015-12-06 16:32:07 +02:00
Tim
49669dc7e0 Merge branch 'dev' 2015-12-06 16:16:39 +02:00
Tim
5bdf79606e v1.2.10 2015-12-06 16:16:03 +02:00
drzoidberg33
ff3a9e47df Merge pull request #335 from drzoidberg33/security-fixes
Fix count type graphs.
2015-12-06 16:12:28 +02:00
Tim
a18ba24f4a Fix count type graphs. 2015-12-06 16:07:57 +02:00
Tim
baeb744a7c Merge branch 'dev' 2015-12-06 14:53:04 +02:00
Tim
d07add383f v1.2.9 2015-12-06 14:52:19 +02:00
drzoidberg33
a1f18bc133 Merge pull request #334 from drzoidberg33/security-fixes
Escape input on friendy_name change.
2015-12-06 14:41:31 +02:00
Tim
e00c23bc49 Escape input on friendy_name change. 2015-12-06 14:39:50 +02:00
drzoidberg33
0228a356e4 Merge pull request #333 from drzoidberg33/security-fixes
Better sanitization on templates and datatables output.
2015-12-06 14:18:22 +02:00
Tim
b0fa0d534e Better sanitization on templates and datatables output. 2015-12-06 14:09:38 +02:00
Jonathan Wong
1157fda96c Hide Pushalot notifier message logging 2015-12-06 00:52:43 -08:00
Jonathan Wong
bbf774379d Merge branch 'dev' 2015-12-05 23:45:35 -08:00
Jonathan Wong
24c8c4319d v1.2.8 2015-12-05 23:44:02 -08:00
JonnyWong16
3b8f9f5892 Merge pull request #328 from JonnyWong16/miscellaneous-fixes
Fix recently added
2015-12-05 23:28:49 -08:00
Jonathan Wong
b47ccd06f9 Sanitize player name 2015-12-05 23:26:54 -08:00
Jonathan Wong
8c4292f9ac Fix recently added using added at time 2015-12-05 14:23:10 -08:00
JonnyWong16
a8fbf8ab1d Merge pull request #327 from zobe123/dev
add Microsoft Edge platform
2015-12-05 11:05:30 -08:00
zobe123
38e9938666 Add Microsoft Edge platform image. 2015-12-05 13:31:56 +01:00
zobe123
93c4a0652e Revert "Add Image for Microsoft Edge"
This reverts commit d12b57e1de.
2015-12-05 13:29:10 +01:00
zobe123
2fff6647fd Revert "Update msedge.png"
This reverts commit 36f0f60c49.
2015-12-05 13:29:01 +01:00
zobe123
36f0f60c49 Update msedge.png 2015-12-05 13:02:53 +01:00
zobe123
d12b57e1de Add Image for Microsoft Edge 2015-12-05 13:02:19 +01:00
zobe123
deb16428ed Update script.js 2015-12-05 12:57:26 +01:00
drzoidberg33
a75aba4aee Merge pull request #324 from JonnyWong16/miscellaneous-fixes
Only schedule job for recently added or monitor remote access if sett…
2015-12-04 20:12:44 +02:00
Jonathan Wong
bb5aa2be3d Remember previous recently added items
* Only pull metadata if there are new recently added items since the
last check
2015-12-03 19:50:46 -08:00
Jonathan Wong
ef6ef98541 Clean up settings help text and wording 2015-12-03 19:07:24 -08:00
Jonathan Wong
89f581f63e Only schedule job for recently added or monitor remote access if setting enabled 2015-12-03 18:40:18 -08:00
JonnyWong16
6081fa329b Merge pull request #321 from JonnyWong16/miscellaneous-fixes
Miscellaneous fixes
2015-12-03 10:39:12 -08:00
Jonathan Wong
112811f3e2 Fix subject UTF-8 encode for Prowl notifier 2015-12-02 19:18:04 -08:00
Jonathan Wong
ede07595c3 Match newline characters in notification text 2015-12-02 19:15:46 -08:00
JonnyWong16
08d65623dd Merge pull request #306 from JonnyWong16/miscellaneous-fixes
Delete users
2015-12-02 17:05:39 -08:00
Jonathan Wong
7540b5fb34 Fix recently added notification delay 2015-12-01 22:54:58 -08:00
Jonathan Wong
10c54c2d10 Only show IPv4 addresses 2015-12-01 21:57:02 -08:00
Jonathan Wong
b8d9c8cc47 Set blank metadata so recently added check continues when and item isn't found 2015-12-01 21:50:47 -08:00
Jonathan Wong
bac5018b27 Add channel support to Telegram notifier 2015-12-01 21:31:48 -08:00
Jonathan Wong
c65d9898c8 Add icon for Apple tvOS 2015-12-01 20:05:37 -08:00
Jonathan Wong
d7284c40bd Allow unicode in notification subject and body 2015-12-01 20:03:25 -08:00
Jonathan Wong
1c00f82097 Add ability to delete users 2015-11-27 21:13:17 -08:00
Jonathan Wong
c501923f2b Hide Pushalot notifier response logging 2015-11-27 18:13:20 -08:00
Jonathan Wong
81b22a8c36 Merge branch 'dev' 2015-11-27 06:38:31 -08:00
Jonathan Wong
beb66396fe v1.2.7 2015-11-27 06:33:03 -08:00
Jonathan Wong
aaf3de68cf Fix IP address in notifications 2015-11-27 06:29:41 -08:00
Jonathan Wong
c827c9e825 Fix IP logging again
* Really this time...
2015-11-27 12:33:34 +02:00
Tim van der Westhuizen
5100fdbc96 Merge branch 'dev' 2015-11-27 10:53:28 +02:00
Tim van der Westhuizen
fae3f38a88 v1.2.6 2015-11-27 10:52:29 +02:00
Tim van der Westhuizen
a8236222fb Catch null ratingKeys in plexWatch db importer.
Make sure we have a string when parsing the latinToAscii function.
2015-11-27 10:45:25 +02:00
Jonathan Wong
2be4d9b6c9 Merge branch 'dev' 2015-11-26 19:08:46 -08:00
Jonathan Wong
00934b04d2 Fix IP logging again
* Really this time...
2015-11-26 19:06:51 -08:00
Jonathan Wong
1e28b22c70 Merge branch 'dev' 2015-11-25 20:23:44 -08:00
Jonathan Wong
776061605f Fix IP logging again 2015-11-25 20:20:06 -08:00
Jonathan Wong
683e5663e1 Merge branch 'dev' 2015-11-25 19:16:50 -08:00
Jonathan Wong
090011c9a5 v1.2.5 2015-11-25 19:15:40 -08:00
Jonathan Wong
30b11bce98 Actually add video and audio decision to notifications 2015-11-25 19:13:08 -08:00
Jonathan Wong
2a91ec1560 Fix season and episode number notification option return at least one digit 2015-11-25 19:07:23 -08:00
Jonathan Wong
908ce1ff8d Fix IP address logging 2015-11-25 19:02:59 -08:00
Jonathan Wong
fac47ee68b Fix log spam if notifications turned off 2015-11-25 18:14:08 -08:00
Jonathan Wong
0f8c122ee3 Add video and audio decision to notification options 2015-11-25 18:12:20 -08:00
Jonathan Wong
893c91a15d Merge branch 'dev' 2015-11-24 18:51:35 -08:00
Jonathan Wong
6d152cf308 v1.2.4 2015-11-24 18:49:35 -08:00
Jonathan Wong
af2d0446da Hide fix metadata button for now 2015-11-24 18:21:59 -08:00
Jonathan Wong
244b03ba3e No restart required for monitoring remote access 2015-11-23 23:10:49 -08:00
Jonathan Wong
50e0629890 Fix unicode server name 2015-11-23 23:10:31 -08:00
Jonathan Wong
c296b38b78 Fix text glitch on welcome wizard 2015-11-23 23:10:16 -08:00
Jonathan Wong
f7cdfd3f30 Upgrade config file from previous versions 2015-11-23 23:10:01 -08:00
drzoidberg33
44cb2400d0 Remove donate link 2015-11-23 10:56:44 +02:00
Jonathan Wong
d297597fa6 Separate out movie and tv logging 2015-11-22 22:19:37 -08:00
Jonathan Wong
168e74aa23 Add logger for recently added 2015-11-22 22:15:28 -08:00
Jonathan Wong
35fa8a749b Fix PMS IP error message 2015-11-22 12:28:24 -08:00
Jonathan Wong
21a1870884 Fix callback in wrong spot 2015-11-22 12:24:57 -08:00
Jonathan Wong
1d86187f79 Fix "Please verify your server" setting bug 2015-11-22 12:11:22 -08:00
JonnyWong16
558f7873f5 Merge pull request #289 from JonnyWong16/miscellaneous-fixes
Use Plex API to check remote access down
2015-11-22 10:33:30 -08:00
Jonathan Wong
a20a52f5f1 Refresh port mapping before checking remote access 2015-11-22 10:22:15 -08:00
Jonathan Wong
17bb57d5f5 Apply media type toggles to recently added 2015-11-21 10:28:26 -08:00
Jonathan Wong
e6aef01508 Separate out TV notification toggle in settings 2015-11-21 10:27:51 -08:00
Jonathan Wong
013d957e47 Use Plex API to check remote access down 2015-11-21 10:13:26 -08:00
Jonathan Wong
99e064e040 Add wording for Pushover group key 2015-11-20 19:04:54 -08:00
JonnyWong16
9e5334ac81 Merge pull request #282 from JonnyWong16/miscellaneous-fixes
IPv6 support, fix recently added, get server name, notify server down
2015-11-20 06:28:02 -08:00
Jonathan Wong
fd43cf5dd4 Add setting to enable remote access monitoring 2015-11-19 19:40:50 -08:00
Jonathan Wong
9aea663754 Logger info for email notification sent 2015-11-17 23:39:02 -08:00
Jonathan Wong
223e2b2b32 Add notification for Plex external port down 2015-11-17 23:38:46 -08:00
Jonathan Wong
ca91adbd53 Add check for Plex external port down 2015-11-17 23:35:41 -08:00
Jonathan Wong
4fffbf8a0c Add server uptime 2015-11-17 19:31:13 -08:00
Jonathan Wong
c3ea35806e Move check for server down to check_active_sessions 2015-11-17 06:57:02 -08:00
Jonathan Wong
1c4df69e61 Toggle bell icon for on_down 2015-11-16 23:52:03 -08:00
Jonathan Wong
707c30b0af Fix missing pmsconnect 2015-11-16 23:28:43 -08:00
Jonathan Wong
4d87666a42 Schedule job to check if server down 2015-11-16 23:19:59 -08:00
Jonathan Wong
b28ac1543a Add notification for server down 2015-11-16 23:19:37 -08:00
Jonathan Wong
1983597cf1 Fix get_server_friendly_name 2015-11-16 22:51:21 -08:00
Jonathan Wong
69a3b5134f Switch recently added to only use activity pinger
* Fixed activity pinger logic for grouping notifications
2015-11-16 21:19:04 -08:00
Jonathan Wong
53044c75dd Add setting for recently added notification delay 2015-11-16 21:17:47 -08:00
Jonathan Wong
12056ac2ba Get server name as a scheduled task 2015-11-16 18:31:22 -08:00
Jonathan Wong
7c8fb58600 Change input to textarea for notification body 2015-11-15 23:51:31 -08:00
Jonathan Wong
da2e7635bd Fix notification tags removal for show and artist 2015-11-15 23:50:58 -08:00
Jonathan Wong
f8b75eadc6 Improved get server friendly name
* Add server friendly name to page title
2015-11-15 21:35:53 -08:00
Jonathan Wong
e9bc767c3b Fix username for database queries
* Get the updated username from the users table instead of the one
stored in the session_history table.
2015-11-15 20:59:04 -08:00
Jonathan Wong
1644bbd4b7 Check for IPv4 mapped IPv6 address in PMS logs 2015-11-15 10:26:03 -08:00
Jonathan Wong
18b328a387 Fix private IP address for IPv6 2015-11-15 10:17:28 -08:00
Tim van der Westhuizen
98411f0715 Move IP geolocation service. Temporary until a suitable replacement can be found. 2015-11-13 17:51:25 +02:00
JonnyWong16
fcb3474312 Merge pull request #278 from JonnyWong16/miscellaneous-fixes
Add IP address to current activity
2015-11-12 06:13:08 -08:00
Jonathan Wong
6362b51902 Add ip_address to notification options
* Also track_name
2015-11-11 14:48:59 -08:00
Jonathan Wong
d79d5d5b39 Better IP address handling for current activity 2015-11-11 09:16:28 -08:00
Jonathan Wong
80df8f6191 Add IP address to sessions for PMS 0.9.14 2015-11-11 08:59:55 -08:00
Jonathan Wong
fd9cf7017b Add IP address to current activity 2015-11-10 19:28:14 -08:00
JonnyWong16
2eed2d54ca Merge pull request #274 from JonnyWong16/miscellaneous-fixes
Add more metadata options to notifications
2015-11-09 19:16:08 -08:00
Jonathan Wong
0c33e7492a Open notifier description links in new window 2015-11-09 17:55:37 -08:00
Jonathan Wong
dd137e5c36 Minor notifier text changes 2015-11-08 15:44:59 -08:00
Jonathan Wong
dea9663adf Add more metadata options to notifications 2015-11-08 15:38:30 -08:00
Tim van der Westhuizen
767dd20bdc Make sure we set the PMS client identifier when auto verifying servers at first run. 2015-11-06 17:18:22 +02:00
Tim
c350943041 Add some debug logging for websocket timeline events. 2015-11-05 22:49:05 +02:00
drzoidberg33
c60340d88b Merge pull request #269 from JonnyWong16/miscellaneous-fixes
Miscellaneous fixes
2015-11-02 23:17:41 +02:00
drzoidberg33
276c0e5c7d Merge pull request #267 from onedr0p/feature/telegram_notifications
Feature/telegram notifications
2015-11-02 23:11:55 +02:00
Jonathan Wong
054f116017 Fix title encode for Pushover notifications 2015-11-01 20:38:22 -08:00
Jonathan Wong
e9017a8342 Escape double quotes in search query 2015-11-01 20:37:28 -08:00
Devin Buhl
9cff20ca16 set text format on encode 2015-11-01 11:03:36 -05:00
Devin Buhl
6cbfacaeae added event / suject to notifcation text 2015-10-31 01:59:13 -04:00
devin
8ebfa20db0 Fixed notifications, and added strings to describe bot token and chat id 2015-10-28 20:36:09 -04:00
devin
5beb4876fb removed telegram lib, updated wording, and used a simple request for sending the notif 2015-10-28 20:28:41 -04:00
devin
c723d33d38 telegram updates to notifiers.py 2015-10-28 07:31:07 -04:00
devin
f75fca12c8 telegram updates to config.py 2015-10-28 07:30:41 -04:00
devin
8671707e4d added telegram lib from repo leandrotoledo/python-telegram-bot 2015-10-28 07:29:57 -04:00
drzoidberg33
a9316ebea1 Merge pull request #265 from drzoidberg33/ip-lookup-provider
Change IP lookup provider
2015-10-28 13:06:35 +02:00
Tim van der Westhuizen
ef86740466 Change order of location details in IP lookup. 2015-10-27 15:25:40 +02:00
Tim van der Westhuizen
57cb755668 Change IP lookup provider to Telize.com which supply an SSL endpoint. 2015-10-27 13:53:47 +02:00
JonnyWong16
aa75cf2b73 Merge pull request #259 from JonnyWong16/recently-added
Recently added
2015-10-26 18:04:11 -07:00
Jonathan Wong
3f8224fec5 List notifications agents alphabetically 2015-10-26 18:00:51 -07:00
Jonathan Wong
0b67abb2a2 Fix typos 2015-10-26 17:52:51 -07:00
Jonathan Wong
872ef2771e Fix double recently added episode notifications 2015-10-25 22:00:51 -07:00
Jonathan Wong
3b457304e9 Fix build_notify_text again
* Separate session and timeline
2015-10-25 21:12:19 -07:00
Jonathan Wong
974c672a87 Format code 2015-10-25 17:38:41 -07:00
Jonathan Wong
b9f47df930 Update activity_pinger for recently added 2015-10-25 17:21:49 -07:00
Jonathan Wong
4c388f60d6 Add config to group recently added by grandparent 2015-10-25 13:30:25 -07:00
Jonathan Wong
d6b31dc542 Set on_created notification in database 2015-10-25 12:20:00 -07:00
Jonathan Wong
539cd60e92 Fix notification_handler 2015-10-24 21:23:19 -07:00
Jonathan Wong
056bcd1488 Update notification handler for timeline events 2015-10-24 21:23:03 -07:00
Jonathan Wong
1c58a47073 Update websocket and handler for timeline events 2015-10-24 21:22:46 -07:00
Jonathan Wong
32cf1ada8b Add config settings for on_created
* Note: on_created is recently added
2015-10-24 21:22:29 -07:00
Jonathan Wong
968132099e Change metadata 'type' to 'media_type' 2015-10-24 21:18:09 -07:00
Tim van der Westhuizen
7b3874dcaa Add Plex Media Player icon. 2015-10-23 11:37:13 +02:00
Tim van der Westhuizen
0302b2412a Fix platform overrides for graphs. 2015-10-22 18:12:50 +02:00
Tim van der Westhuizen
4ac5329019 Add Konvergo to platform name overrides. 2015-10-22 17:45:23 +02:00
Tim van der Westhuizen
37bc68573c Implement IFTTT notification option. PR #241. 2015-10-22 16:04:31 +02:00
Tim
dc8996c4d2 Escape single quotes for usernames on user stats page. 2015-10-21 22:06:02 +02:00
drzoidberg33
1ef9d72534 Merge pull request #256 from JonnyWong16/miscellaneous-fixes
Miscellaneous fixes
2015-10-20 13:15:49 +02:00
Jonathan Wong
db7225fbad Clean up code 2015-10-20 00:48:26 -07:00
Jonathan Wong
2c354ad783 Fix bug in search query 2015-10-18 17:47:21 -07:00
Jonathan Wong
b96abc8853 Add detailed logger messages for database queries 2015-10-18 13:53:23 -07:00
Jonathan Wong
c4dc81e8fb Change order args are created for datatable query
* Make sure search filter args are created after custom_where
2015-10-18 13:29:29 -07:00
Jonathan Wong
be753983fe Fix breadcrumbs for update metadata page 2015-10-18 13:14:22 -07:00
Jonathan Wong
1bcb34d7eb Add button to fix metadata on info pages 2015-10-18 13:02:16 -07:00
Jonathan Wong
2243cd1de9 Add filtering of media_type from history table 2015-10-18 11:50:01 -07:00
Jonathan Wong
1ff58a85dc Fix bug in "Last Watched" statistics
*Fix if two users watched the same item, it would only show the most
recent user.
2015-10-18 10:27:18 -07:00
49 changed files with 3423 additions and 1376 deletions

1
.gitignore vendored
View File

@@ -21,6 +21,7 @@ cache/*
*.crt
*.key
*.csr
*.pem
# OS generated files #
######################

View File

@@ -1,5 +1,110 @@
# Changelog
## v1.2.15 (2015-12-20)
* Fix navbar covering current activity on smaller screens.
* Fix metadata for grouped recently added notifications.
* Fix Growl notification agent not working.
* Change graph days selection.
* Change watch statistics to match table history grouping.
* Add automatic discovery of Pushbullet devices.
* Add Most Concurrent Streams watch statistic.
* Add precentage to current activity progress bars.
* Add a bunch of stream details to notification options.
* Add notification for Plex Remote Access/Plex Media Server back up.
* Add CC/BCC and multiple recipients to email notification agent.
* Add total watch time to history table footer.
## v1.2.14 (2015-12-07)
* Fix regression with PlexWatch db importer and buffer warnings.
## v1.2.13 (2015-12-06)
* Fix match newlines between tags in notification text.
* Fix current activity not showing on PMS 0.9.12.
## v1.2.12 (2015-12-06)
* Fix for "too many open files" error.
## v1.2.11 (2015-12-06)
* Fix more regressions (sorry).
## v1.2.10 (2015-12-06)
* Fix broken count graphs regression.
## v1.2.9 (2015-12-06)
* Fix and improve text sanitization.
## v1.2.8 (2015-12-06)
* Fix sanitize player names
* Fix recently added notification delay
* Fix recently added metadata queries
* Fix multiple lines in notification body text
* Fix UTF-8 encoding in Prowl notifications subject line
* Change to only log IPv4 addresses
* Add global toggle for recently added notifcations
* Add feature to delete users
* Add channel support for Telegram notification agent
* Add icon for Apple tvOS
* Add icon for Microsoft Edge
## v1.2.7 (2015-11-27)
* Fix IP address option in notifications
## v1.2.6 (2015-11-27)
* Fixes for IP logging in PMS < 0.9.14.x.
* Fix issue in plexWatch importer when trying to import item with no ratingKey.
## v1.2.5 (2015-11-25)
* Add video_decision and audio_decision to notification options
* Fix IP address logging
* Fix log spam if notifications disabled
## v1.2.4 (2015-11-24)
* Add filtering by media type in the history table
* Add IFTTT notification agent
* Add Telegram notification agent
* Add notifications for recently added media
* Add notifications for server down and remote access down
* Add more metadata to notifications options
* Add IP address to notification options (for PMS 0.9.14 and above)
* Add server uptime to notification options
* Add IP address to current activity
* Add IPv6 address logging
* Add PMS server name to the page title
* Fix bug in "Last Watched" statistic
* Fix bug in search query
* Fix bug on user pages for usernames with single quotes
* Fix name for new Plex Media Center
* Fix Pushover notifications with unicode characters
* Fix bug with showing old usernames in datatables
* Fix bug with "Please verify your server" in settings
* Change IP lookup provider
* Change notifications custom body text to larger text box
* Change movie/tv logging and notifications into individual options
## v1.2.3 (2015-10-18)
* Added "remaining time" as notification substitution.

View File

@@ -6,11 +6,7 @@ A python based web application for monitoring, analytics and notifications for P
This project is based on code from Headphones (https://github.com/rembo10/headphones) and PlexWatchWeb (https://github.com/ecleese/plexWatchWeb).
* 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. 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)
* PlexPy forum thread: https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program
###Support

View File

@@ -7,7 +7,7 @@ from plexpy import version
<html lang="en">
<head>
<meta charset="utf-8">
<title>PlexPy - ${title}</title>
<title>PlexPy - ${title} | ${server_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
@@ -57,11 +57,11 @@ from plexpy import version
</div>
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
<ul class="nav navbar-nav">
<li>
<li class="hidden-sm hidden-xs">
<form action="search" method="post" class="form" id="search_form">
<div class="input-group">
<span class="input-textbox">
<input type="text" class="form-control" name="search_query" id="search_query" aria-label="Search" placeholder="Search..."/>
<input type="text" class="form-control" name="query" id="query" aria-label="Search" placeholder="Search..."/>
</span>
<span class="input-group-btn">
<button class="btn btn-dark btn-inactive" type="submit" id="search_button"><i class="fa fa-search"></i></button>
@@ -131,21 +131,21 @@ ${next.headerIncludes()}
</script>
<script>
$('#search_form').submit(function (e) {
if ($('#search_query').hasClass('active') && $('#search_query').val().trim() != '') {
if ($('#query').hasClass('active') && $('#query').val().trim() != '') {
$.ajax({
type: 'post',
url: 'search',
data: { 'query': $('#search_query').val() }
data: { 'query': $('#query').val() }
})
} else {
e.preventDefault();
$('#search_button').removeClass('btn-inactive');
$('#search_query').clearQueue().val('').animate({ right: '0', width: '250px' }).addClass('active').focus();
$('#query').clearQueue().val('').animate({ right: '0', width: '200px' }).addClass('active').focus();
}
})
$('#search_query').on('blur', function (e) {
$('#query').on('blur', function (e) {
if ($(this).val().trim() == '') {
$(this).delay(200).animate({ right: '-250px', width: '0' }, function () {
$(this).delay(200).animate({ right: '-200px', width: '0' }, function () {
$('#search_button').addClass('btn-inactive');
}).removeClass('active');
}

View File

@@ -341,6 +341,24 @@ input[type="color"],
border-radius: 3px;
transition: background-color .3s;
}
textarea.form-control {
height: initial;
margin: 5px 0 5px 0;
color: #fff;
border: 0px solid #444;
background: #555;
padding: 6px 12px;
background-color: #555;
border-radius: 3px;
transition: background-color .3s;
resize: none;
}
textarea.form-control:focus {
outline: 0;
color: #555;
background-color: #fff;
transition: background-color .3s;
}
.pagination > li > a,
.pagination > li > span {
position: relative;
@@ -469,7 +487,8 @@ input[type="color"],
.users-poster-face {
overflow: hidden;
float: left;
background-size: contain;
background-size: cover;
background-position: center;
height: 40px;
width: 40px;
-webkit-border-radius: 50%;
@@ -688,6 +707,14 @@ a:hover .dashboard-activity-poster {
white-space: nowrap;
width: 150px;
}
.dashboard-activity-poster-info-ip-address {
position: absolute;
bottom: 5px;
left: 10px;
text-align: left;
font-size: 12px;
color: #eee;
}
.dashboard-activity-poster-info-time {
position: absolute;
bottom: 5px;
@@ -711,14 +738,20 @@ a:hover .dashboard-activity-poster {
transition: all 0s;
}
.dashboard-activity-progress .bufferbar {
padding-top: 6px;
padding-right: 3px;
font-size: x-small;
text-align: right;
color: rgba(255, 255, 255, 0);
background-color: #444;
position: absolute;
height: 6px;
overflow: hidden;
}
.dashboard-activity-progress .bar {
padding-top: 6px;
padding-right: 3px;
font-size: x-small;
text-align: right;
color: rgba(255, 255, 255, 0);
background-color: #faa732;
background-image: -moz-linear-gradient(top, #fbb450, #f89406);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#fbb450), to(#f89406));
@@ -731,6 +764,37 @@ a:hover .dashboard-activity-poster {
height: 6px;
overflow: hidden;
}
.dashboard-instance.hover .dashboard-activity-progress-bar {
height: 14px;
transform-origin: top;
transition: all .2s ease;
border-radius: 0px 0px 3px 3px;
}
.dashboard-instance.hover .bar {
height: 14px;
transform-origin: top;
transition: all .2s ease;
border-radius: 0px 0px 3px 3px;
color: rgba(255, 255, 255, 1);
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25),0%,rgba(0,0,0,0),50px);
background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
}
.dashboard-instance.hover .bufferbar {
height: 14px;
transform-origin: top;
transition: all .2s ease;
border-radius: 0px 0px 3px 3px;
color: rgba(255, 255, 255, 1);
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25),0%,rgba(0,0,0,0),50px);
background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
}
.dashboard-instance.hover .dashboard-activity-metadata-wrapper {
margin-top: 11px;
transform-origin: top;
transition: all .2s ease;
}
.dashboard-activity-metadata-wrapper {
position: relative;
width: 100%;
@@ -1474,7 +1538,8 @@ a:hover .item-children-poster {
float: left;
margin-top: 15px;
margin-right: 15px;
background-size: contain;
background-size: cover;
background-position: center;
height: 80px;
width: 80px;
-webkit-border-radius: 50%;
@@ -2348,7 +2413,8 @@ a .home-platforms-instance-list-oval:hover,
top: 5px;
left: 12px;
}
#users-to-delete > li {
#users-to-delete > li,
#users-to-purge > li {
color: #e9a049;
}
#updatebar {
@@ -2462,27 +2528,27 @@ table[id^='history_child'] thead th {
overflow: hidden;
}
#search_form {
width: 350px;
width: 300px;
padding: 8px 15px;
}
#search_form span.input-textbox {
overflow: hidden;
width: 250px;
width: 200px;
height: 34px;
display: inline-flex;
float: right;
}
#search_form #search_query {
#search_form #query {
width: 0;
height: 34px;
margin-top: 0;
float: right;
position: relative;
right: -250px;
right: -200px;
border-radius: 3px 0 0 3px;
}
#search_form #search_query.active {
width: 250px;
#search_form #query.active {
width: 200px;
right: 0px;
}
#search_form #search_button.btn-inactive {
@@ -2493,4 +2559,38 @@ table[id^='history_child'] thead th {
-ms-transition: background 0.3s;
-o-transition: background 0.3s;
transition: background 0.3s;
}
}
.notification-params {
margin-top: 10px;
background-color: #282828;
}
.notification-params th {
padding-left: 10px;
height: 30px;
}
.notification-params td {
height: 25px;
}
.notification-params td:first-child {
padding-left: 10px;
width: 200px;
}
.notification-params td:not(:first-child) {
padding-right: 10px;
}
.notification-params tr:nth-child(odd) td {
background-color: rgba(255,255,255,0.035);
}
.notification-params tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010);
}
#days-selection label {
margin-bottom: 0;
}
#graph-days {
margin: 0;
width: 75px;
height: 34px;
}

View File

@@ -32,6 +32,7 @@ user_thumb Returns the profile picture of the user owning the s
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.
ip_address Returns the ip address of the stream.
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.
@@ -191,6 +192,13 @@ DOCUMENTATION :: END
</div>
% if a['media_type'] != 'photo':
<div class="dashboard-activity-poster-info-bar">
<div class="dashboard-activity-poster-info-ip-address">
% if a['ip_address']:
<span>IP: ${a['ip_address']}</span>
% else:
<span>IP: N/A</span>
% endif
</div>
<div class="dashboard-activity-poster-info-time">
<span class="progress_time">${a['view_offset']}</span>/<span class="progress_time">${a['duration']}</span>
</div>
@@ -272,6 +280,13 @@ DOCUMENTATION :: END
e.preventDefault();
$($(this).attr('data-target')).toggle();
});
// Add hover class to dashboard-instance
$('.dashboard-activity-poster').hover(function() {
$(this).closest('.dashboard-instance').addClass('hover');
}, function() {
$(this).closest('.dashboard-instance').removeClass('hover');
});
</script>
% else:
<div class="text-muted">Nothing is currently being watched.</div><br>

View File

@@ -15,11 +15,13 @@ DOCUMENTATION :: END
</%doc>
% if data != None:
% if data == '0':
<h3>Activity</h3>
% if data == '0':
<h3>Activity</h3>
% elif data == '1':
<h3>Activity <small>${data} stream</small></h3>
% else:
<h3>Activity <small>${data} streams</small></h3>
% endif
% else:
<h3>Activity <small>${data} stream(s)</small></h3>
<h3>Activity</h3>
% endif
% else:
<h3>Activity</h3>
% endif

View File

@@ -115,7 +115,7 @@ DOCUMENTATION :: END
success: function(data) {
$("#edit-user-status-message").html(data);
if ($.trim(friendly_name) !== '') {
$(".set-username").html(friendly_name);
$('.set-username').html(document.createTextNode(friendly_name));
}
$("#user-profile-thumb").attr('src', thumb);
}

View File

@@ -13,110 +13,132 @@
</div>
<div class="button-bar hidden-xs">
<div class="btn-group" data-toggle="buttons" id="yaxis-selection">
% if config['graph_type'] == 'duration':
<label class="btn btn-dark">
<input type="radio" name="yaxis-options" id="yaxis-count" value="plays" autocomplete="off"> Play Count
</label>
<label class="btn btn-dark active">
<input type="radio" name="yaxis-options" id="yaxis-duration" value="duration" autocomplete="off" checked> Play Duration
</label>
% else:
<label class="btn btn-dark active">
<input type="radio" name="yaxis-options" id="yaxis-count" value="plays" autocomplete="off" checked> Play Count
</label>
<label class="btn btn-dark">
<input type="radio" name="yaxis-options" id="yaxis-duration" value="duration" autocomplete="off"> Play Duration
</label>
% endif
</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>
<label class="btn btn-dark">
<input type="radio" name="date-options" id="graph-90" value="90" autocomplete="off"> 90 days
</label>
<label class="btn btn-dark">
<input type="radio" name="date-options" id="graph-365" value="365" autocomplete="off"> 1 year
<div class="btn-group" id="days-selection">
<label>
<input type="number" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" /> days
</label>
</div>
</div>
</div>
<div class='table-card-back'>
<ul class="nav nav-pills" role="tablist" id="graph-tabs">
% if config['graph_tab'] == 'tabs-3':
<li role="presentation"><a href="#tabs-1" aria-controls="tabs-1" data-toggle="tab" role="tab">Plays by period</a></li>
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" data-toggle="tab" role="tab">Stream Info</a></li>
<li role="presentation" class="active"><a href="#tabs-3" aria-controls="tabs-3" data-toggle="tab" role="tab">Play Totals</a></li>
% elif config['graph_tab'] == 'tabs-2':
<li role="presentation"><a href="#tabs-1" aria-controls="tabs-1" data-toggle="tab" role="tab">Plays by period</a></li>
<li role="presentation" class="active"><a href="#tabs-2" aria-controls="tabs-2" data-toggle="tab" role="tab">Stream Info</a></li>
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" data-toggle="tab" role="tab">Play Totals</a></li>
% else:
<li role="presentation" class="active"><a href="#tabs-1" aria-controls="tabs-1" data-toggle="tab" role="tab">Plays by period</a></li>
<li role="presentation"><a href="#tabs-2" aria-controls="tabs-2" data-toggle="tab" role="tab">Stream Info</a></li>
<li role="presentation"><a href="#tabs-3" aria-controls="tabs-3" data-toggle="tab" role="tab">Play Totals</a></li>
% endif
</ul>
<div class="tab-content">
% if config['graph_tab'] != 'tabs-2' and config['graph_tab'] != 'tabs-3':
<div role="tabpanel" class="tab-pane active" id="tabs-1">
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The total play count or duration of tv, movies, and music played per day. Click a graph point to open up a list of items played for that specific date.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_day">
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...</div>
<br>
% else:
<div role="tabpanel" class="tab-pane" id="tabs-1">
% endif
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-history"></i> Daily <span class="yaxis-text">Play count</span> <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The total play count or duration of tv, movies, and music played per day. Click a graph point to open up a list of items played for that specific date.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_day">
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...</div>
<br>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h4><i class="fa fa-calendar"></i> <span class="yaxis-text">Play count</span> by day of week <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played per day of the week.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_dayofweek" style="float: left;">
<div class="graphs-load">
<i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
<div class="col-md-6">
<h4><i class="fa fa-clock-o"></i> <span class="yaxis-text">Play count</span> by hour of day <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played per hour of the day.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_hourofday">
<div class="graphs-load">
<i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by top 10 platforms <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played by top 10 most active platforms.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_platform" style="float: left;">
<div class="graphs-load">
<i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
<div class="col-md-6">
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by top 10 users <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played by top 10 most active users.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_user">
<div class="graphs-load">
<i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h4><i class="fa fa-calendar"></i> <span class="yaxis-text">Play count</span> by day of week <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played per day of the week.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_dayofweek" style="float: left;">
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
<div class="col-md-6">
<h4><i class="fa fa-clock-o"></i> <span class="yaxis-text">Play count</span> by hour of day <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played per hour of the day.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_hourofday">
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<h4><i class="fa fa-television"></i> <span class="yaxis-text">Play count</span> by top 10 platforms <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played by top 10 most active platforms.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_platform" style="float: left;">
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
<div class="col-md-6">
<h4><i class="fa fa-user"></i> <span class="yaxis-text">Play count</span> by top 10 users <small>Last <span class="days">30</span> days</small></h4>
<p class="help-block">
The combined total of tv, movies, and music played by top 10 most active users.
</p>
<div class="graphs-instance">
<div class="watch-chart" id="chart_div_plays_by_user">
<div class="graphs-load"><i class="fa fa-refresh fa-spin"></i> Loading chart...
</div>
<br>
</div>
</div>
</div>
</div>
</div>
% if config['graph_tab'] == 'tabs-2':
<div role="tabpanel" class="tab-pane active" id="tabs-2">
% else:
<div role="tabpanel" class="tab-pane" id="tabs-2">
% endif
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-video-camera"></i> Daily Stream type breakdown <small>Last <span class="days">30</span> days</small></h4>
@@ -189,7 +211,11 @@
</div>
</div>
% if config['graph_tab'] == 'tabs-3':
<div role="tabpanel" class="tab-pane active" id="tabs-3">
% else:
<div role="tabpanel" class="tab-pane" id="tabs-3">
% endif
<div class="row">
<div class="col-md-12">
<h4><i class="fa fa-calendar"></i> Plays by Month <small>Last 12 months</small></h4>
@@ -263,36 +289,12 @@
<script>
$(document).ready(function () {
// 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 from config
var yaxis = "${config['graph_type']}";
var current_range = ${config['graph_days']};
var current_tab = "${'#' + config['graph_tab']}";
// 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');
}
$('.days').html(current_range);
var music_visible = (${config['music_logging_enable']} == 1 ? true : false);
@@ -476,40 +478,61 @@
}
// Set initial state
loadGraphsTab1(current_range, yaxis);
if (current_tab == '#tabs-1') { loadGraphsTab1(current_range, yaxis); }
if (current_tab == '#tabs-2') { loadGraphsTab2(current_range, yaxis); }
if (current_tab == '#tabs-3') { loadGraphsTab3(yaxis); }
// Tab1 opened
$('#graph-tabs a[href="#tabs-1"]').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
$('#days-selection').show();
loadGraphsTab1(current_range, yaxis);
$.ajax({
url: 'set_graph_config',
data: { graph_tab: current_tab.replace('#','') },
async: true
});
})
// Tab2 opened
$('#graph-tabs a[href="#tabs-2"]').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
$('#days-selection').show();
loadGraphsTab2(current_range, yaxis);
$.ajax({
url: 'set_graph_config',
data: { graph_tab: current_tab.replace('#','') },
async: true
});
})
// Tab3 opened
$('#graph-tabs a[href="#tabs-3"]').on('shown.bs.tab', function (e) {
e.preventDefault();
current_tab = $(this).attr('href');
$('#days-selection').hide();
console.log('loading....');
loadGraphsTab3(yaxis);
$.ajax({
url: 'set_graph_config',
data: { graph_tab: current_tab.replace('#','') },
async: true
});
})
// Date range changed
$('#days-selection').on('change', function() {
current_range = $('input[name=date-options]:checked', '#days-selection').val();
$('#graph-days').on('change', function() {
current_range = $(this).val();
if (current_range < 1) {
$(this).val(7);
current_range = 7;
}
if (current_tab == '#tabs-1') { loadGraphsTab1(current_range, yaxis); }
if (current_tab == '#tabs-2') { loadGraphsTab2(current_range, yaxis); }
$('.days').html(current_range);
$.ajax({
url: 'set_graph_config',
data: { graph_days: current_range},
async: true
});
});
// Y-axis changed
@@ -518,6 +541,11 @@
if (current_tab == '#tabs-1') { loadGraphsTab1(current_range, yaxis); }
if (current_tab == '#tabs-2') { loadGraphsTab2(current_range, yaxis); }
if (current_tab == '#tabs-3') { loadGraphsTab3(yaxis); }
$.ajax({
url: 'set_graph_config',
data: { graph_type: yaxis},
async: true
});
});
function setGraphFormat(type) {

View File

@@ -76,19 +76,50 @@
<script src="interfaces/default/js/moment-with-locale.js"></script>
<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 ) };
$(document).ready(function () {
function loadHistoryTable(media_type) {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function (d) {
return {
'json_data': JSON.stringify(d),
'media_type': media_type
};
}
}
}
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, 11] });
$(colvis.button()).appendTo('div.colvis-button-bar');
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, 11] });
$(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table', history_table);
clearSearchButton('history_table', history_table);
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
<label class="btn btn-dark active"> \
<input type="radio" name="media_type-filter" id="history-all" value="all" autocomplete="off"> All \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-movies" value="movie" autocomplete="off"> Movies \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-tv_shows" value="episode" autocomplete="off"> TV Shows \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-music" value="track" autocomplete="off"> Music \
</label> \
</div>');
$('#media_type-selection').on('change', function () {
$('#media_type-selection > label').removeClass('active');
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
$(selected_filter).closest('label').addClass('active');
media_type = $(selected_filter).val();
history_table.draw();
});
}
var media_type = 'all';
loadHistoryTable(media_type);
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);

View File

@@ -39,11 +39,16 @@ 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' or 'last_watched' ==
platform_type Returns the platform name for the associated stat.
player Returns the player name for the associated stat.
== Only if 'stat_id' is 'last_watched' ==
last_watch Returns the time the media item was last watched.
== Only if 'stat_id' is 'most_concurrent' ==
count Returns the count of the most concurrent streams.
started Returns the start time of the most concurrent streams.
stopped Returns the stop time of the most concurrent streams.
DOCUMENTATION :: END
</%doc>
@@ -709,7 +714,7 @@ DOCUMENTATION :: END
<script>
$('#last-watch-stat').text(moment(${top_stat['rows'][0]['last_watch']},"X").format(date_format));
</script>
</span> - ${top_stat['rows'][0]['platform_type']}
</span> - ${top_stat['rows'][0]['player']}
</p>
</div>
</div>
@@ -755,7 +760,7 @@ DOCUMENTATION :: END
<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']}
</span> - ${top_stat['rows'][loop.index]['player']}
</p>
</div>
</div>
@@ -782,6 +787,30 @@ DOCUMENTATION :: END
% endif
</li>
</div>
% elif top_stat['stat_id'] == 'most_concurrent' and top_stat['rows']:
<div class="home-platforms-instance">
<li>
<div class="home-platforms-instance-info">
<div class="home-platforms-instance-name">
<h4>Most Concurrent Streams</h4>
</div>
<div class="home-platforms-instance-playcount">
<h4>
<span id="most-concurrent-start">
<script>
$('#most-concurrent-start').text(moment(${top_stat['rows'][0]['started']},"X").format(date_format + ' ' + time_format));
</script>
</span>
</h4>
<h3>${top_stat['rows'][0]['count']}</h3>
<p> streams</p>
</div>
</div>
<div class="home-platforms-instance-poster">
<div class="home-platforms-instance-box" style="background-image: url(interfaces/default/images/home-stat_most-concurrent.png);"></div>
</div>
</li>
</div>
% endif
% endfor
</ul>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -33,7 +33,7 @@
<div class="row">
<div class="col-md-12">
<div class="padded-header" id="library-statistics-header">
<h3>Library Statistics</h3>
<h3>Library Statistics <small>${config['pms_name']}</small></h3>
</div>
<div id="library-stats" class="library-platforms">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading stats...</div>
@@ -98,27 +98,6 @@
});
}
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',
@@ -170,7 +149,6 @@
});
getHomeStats();
getLibraryStatsHeader();
getLibraryStats();

View File

@@ -11,7 +11,7 @@ data :: Usable parameters (if not applicable for media type, blank value will be
== Global keys ==
rating_key Returns the unique identifier for the media item.
type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'.
media_type Returns the type of media. Either 'movie', 'show', 'season', 'episode', 'artist', 'album', or 'track'.
art Returns the location of the item's artwork
title Returns the name of the movie, show, episode, artist, album, or track.
duration Returns the standard runtime of the media.
@@ -60,7 +60,7 @@ DOCUMENTATION :: END
% if data:
<div class="container-fluid">
<div class="row">
% if data['type'] != 'library':
% if data['media_type'] != 'library':
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div>
% endif
<div class="summary-container">
@@ -68,7 +68,7 @@ DOCUMENTATION :: END
<div class="col-md-12">
<div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb">
% if data['type'] == 'library':
% if data['media_type'] == 'library':
% if data['library'] == 'movie':
<li class="active">Movies</li>
% elif data['library'] == 'show':
@@ -76,29 +76,29 @@ DOCUMENTATION :: END
% elif data['library'] == 'artist':
<li class="active">Music</li>
% endif
% elif data['type'] == 'movie':
% elif data['media_type'] == 'movie':
<li><a href="info?item_id=movie">Movies</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'show':
% elif data['media_type'] == 'show':
<li><a href="info?item_id=show">TV Shows</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'season':
% elif data['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">Season ${data['index']}</li>
% elif data['type'] == 'episode':
% elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">Season ${data['parent_index']}</a></li>
<li class="active">Episode ${data['index']} - ${data['title']}</li>
% elif data['type'] == 'artist':
% elif data['media_type'] == 'artist':
<li><a href="info?item_id=artist">Music</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'album':
% elif data['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
<li class="active">${data['title']}</li>
% elif data['type'] == 'track':
% elif data['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li class="hidden-xs hidden-sm"><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li>
<li><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></li>
@@ -108,28 +108,28 @@ DOCUMENTATION :: END
</div>
</div>
</div>
% if data['type'] != 'library':
% if data['media_type'] != 'library':
<div class="summary-content-title-wrapper">
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['type'] == 'track':
% if data['media_type'] == 'track':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="Plex/Web" title="View in Plex/Web">
% elif data['type'] != 'library':
% elif data['media_type'] != 'library':
<a href="http://app.plex.tv/web/app#!/server/${config['pms_identifier']}/details/%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="Plex/Web" title="View in Plex/Web">
% endif
% if data['type'] == 'episode':
% if data['media_type'] == 'episode':
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
% elif data['type'] == 'artist' or data['type'] == 'album' or data['type'] == 'track':
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
% elif data['type'] != 'library':
% elif data['media_type'] != 'library':
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);">
<div class="summary-poster-face-overlay">
<span></span>
@@ -139,19 +139,19 @@ DOCUMENTATION :: END
</a>
</div>
<div class="summary-content-title">
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'artist':
% if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'artist':
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['type'] == 'season':
% elif data['media_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':
% elif data['media_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>
% elif data['type'] == 'album':
% elif data['media_type'] == 'album':
<h1><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
<h2>${data['title']}</h2>
% elif data['type'] == 'track':
% elif data['media_type'] == 'track':
<h1><a href="info?item_id=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1>
<h2><a href="info?item_id=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2>
<h3 class="hidden-xs">T${data['index']}</h3>
@@ -161,13 +161,13 @@ DOCUMENTATION :: END
</div>
% endif
<div class="summary-content-wrapper">
% if data['type'] != 'library':
% if data['media_type'] != 'library':
<div class="col-md-9">
% if data['type'] == 'movie' or data['type'] == 'show' or data['type'] == 'season':
% if data['media_type'] == 'movie' or data['media_type'] == 'show' or data['media_type'] == 'season':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 275px;"></div>
% elif data['type'] == 'episode':
% elif data['media_type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 40px;"></div>
% elif data['type'] == 'artist' or data['type'] == 'album' or data['type'] == 'track':
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 150px;"></div>
% else:
<div class="summary-content-padding hidden-xs hidden-sm"></div>
@@ -189,13 +189,13 @@ DOCUMENTATION :: END
% endif
</div>
<div class="summary-content-details-tag">
% if data['type'] == 'movie':
% if data['media_type'] == 'movie':
Year <strong> ${data['year']}</strong>
% elif data['type'] == 'show':
% elif data['media_type'] == 'show':
Aired <strong> ${data['year']}</strong>
% elif data['type'] == 'episode':
% elif data['media_type'] == 'episode':
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['type'] == 'album' or data['type'] == 'track':
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
Released <strong> ${data['year']}</strong>
% endif
</div>
@@ -268,7 +268,7 @@ DOCUMENTATION :: END
% endif
</div>
</div>
% if data['type'] == 'show':
% if data['media_type'] == 'show':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
@@ -279,7 +279,7 @@ DOCUMENTATION :: END
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
</div>
</div>
% elif data['type'] == 'season':
% elif data['media_type'] == 'season':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
@@ -290,7 +290,7 @@ DOCUMENTATION :: END
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
</div>
</div>
% elif data['type'] == 'artist':
% elif data['media_type'] == 'artist':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
@@ -301,7 +301,7 @@ DOCUMENTATION :: END
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
</div>
</div>
% elif data['type'] == 'album':
% elif data['media_type'] == 'album':
<div class='col-md-12'>
<div class='table-card-header'>
<div class="header-bar">
@@ -382,49 +382,38 @@ DOCUMENTATION :: END
<div class="summary-navbar">
<div class="col-md-12">
<div class="summary-navbar-list">
% if query:
% if query['media_type'] == 'movie':
<span>Movies</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>${query['title']}</span>
% elif query['media_type'] == 'show':
<span>TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>${query['grandparent_title']}</span>
% elif query['media_type'] == 'season':
<span class="hidden-xs hidden-sm">TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm">${query['grandparent_title']}</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>Season ${query['parent_media_index']}</span>
% elif query['media_type'] == 'episode':
<span class="hidden-xs hidden-sm">TV Shows</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm">${query['grandparent_title']}</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>Season ${query['parent_media_index']}</span>
<span><i class="fa fa-chevron-right"></i></span>
<span>Episode ${query['media_index']} - ${query['title']}</span>
% elif query['media_type'] == 'artist':
<span>Music</span>
<span><i class="fa fa-chevron-right"></i></span>
<span>${query['grandparent_title']}</span>
% elif query['media_type'] == 'album':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>${query['grandparent_title']}</span>
<span><i class="fa fa-chevron-right"></i></span>
<span>${query['parent_title']}</span>
% elif query['media_type'] == 'track':
<span class="hidden-xs hidden-sm">Music</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span class="hidden-xs hidden-sm">${query['grandparent_title']}</span>
<span class="hidden-xs hidden-sm"><i class="fa fa-chevron-right"></i></span>
<span>${query['parent_title']}</span>
<span><i class="fa fa-chevron-right"></i></span>
<span>Track ${query['media_index']} - ${query['title']}</span>
% endif
% endif
<ul class="list-unstyled breadcrumb">
% if query:
% if query['media_type'] == 'movie':
<li><a href="info?item_id=movie">Movies</a></li>
<li class="active">${query['title']}</li>
% elif query['media_type'] == 'show':
<li><a href="info?item_id=show">TV Shows</a></li>
<li class="active">${query['grandparent_title']}</li>
% elif query['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<li class="active">Season ${query['parent_media_index']}</li>
% elif query['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="info?item_id=show">TV Shows</a></li>
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<li>Season ${query['parent_media_index']}</li>
<li class="active">Episode ${query['media_index']} - ${query['title']}</li>
% elif query['media_type'] == 'artist':
<li><a href="info?item_id=artist">Music</a></li>
<li class="active">${query['grandparent_title']}</li>
% elif query['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li>${query['grandparent_title']}</li>
<li class="active">${query['parent_title']}</li>
% elif query['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="info?item_id=artist">Music</a></li>
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<li>${query['parent_title']}</li>
<li class="active">Track ${query['media_index']} - ${query['title']}</li>
% endif
% endif
</ul>
</div>
</div>
</div>
@@ -510,54 +499,54 @@ DOCUMENTATION :: END
% if data:
<script src="interfaces/default/js/tables/history_table.js"></script>
% if data['type'] == 'library':
% if data['media_type'] == 'library':
<script>
function get_history() {
history_table_options.ajax = {
"url": "get_history",
url: 'get_history',
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'media_type': '${data['media_type']}' };
'media_type': "${data['media_type_filter']}" };
}
}
}
</script>
% elif data['type'] == 'show' or data['type'] == 'artist':
% elif data['media_type'] == 'show' or data['media_type'] == 'artist':
<script>
function get_history() {
history_table_options.ajax = {
"url": "get_history",
url: 'get_history',
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'grandparent_rating_key': ${data['rating_key']} };
'grandparent_rating_key': "${data['rating_key']}" };
}
}
}
</script>
% elif data['type'] == 'season' or data['type'] == 'album':
% elif data['media_type'] == 'season' or data['media_type'] == 'album':
<script>
function get_history() {
history_table_options.ajax = {
"url": "get_history",
url: 'get_history',
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'parent_rating_key': ${data['rating_key']} };
'parent_rating_key': "${data['rating_key']}" };
}
}
}
</script>
% elif data['type'] == 'episode' or data['type'] == 'track' or data['type'] == 'movie':
% elif data['media_type'] == 'episode' or data['media_type'] == 'track' or data['media_type'] == 'movie':
<script>
function get_history() {
history_table_options.ajax = {
"url": "get_history",
url: 'get_history',
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'rating_key': ${data['rating_key']} };
'rating_key': "${data['rating_key']}" };
}
}
}
@@ -610,11 +599,11 @@ DOCUMENTATION :: END
});
});
</script>
% if data['type'] == 'show' or data['type'] == 'season' or data['type'] == 'artist' or data['type'] == 'album':
% if data['media_type'] == 'show' or data['media_type'] == 'season' or data['media_type'] == 'artist' or data['media_type'] == 'album':
<script>
$.ajax({
url: 'get_item_children',
type: "GET",
type: 'GET',
async: true,
data: { rating_key : ${data['rating_key']} },
complete: function(xhr, status) {
@@ -622,7 +611,16 @@ DOCUMENTATION :: END
});
</script>
% endif
% if data['type'] != 'library' and data['rating']:
% if data['media_type'] != 'library':
<!--
<script>
$('#row-edit-mode').after('<a href="info?item_id=${data['rating_key']}" class="btn btn-danger btn-edit" id="fix-metadata" \
data-toggle="tooltip" data-placement="left" title="Fix metadata if the item was moved in Plex"><i class="fa fa-wrench"></i> Fix Metadata</a>');
$('#fix-metadata').tooltip();
</script>
-->
% endif
% if data['media_type'] != 'library' and data['rating']:
<script>
// Convert rating to 5 star rating type
var starRating = Math.round(${data['rating']} / 2);
@@ -638,9 +636,9 @@ DOCUMENTATION :: END
<script>
$.ajax({
url: 'get_search_results_children',
type: "GET",
type: 'GET',
async: true,
data: {'query': "${query['query_string']}",
data: {'query': "${query['query_string'].replace('"','\\"')}",
'media_type': "${query['media_type']}",
'season_index': "${query['parent_media_index']}"
},

View File

@@ -17,8 +17,8 @@
<h4><strong>Location Details</strong></h4>
<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>
<li>City: <strong><span id="city"></span></strong></li>
<li>Timezone: <strong><span id="timezone"></span></strong></li>
<li>Latitude: <strong><span id="lat"></span></strong></li>
<li>Longitude: <strong><span id="lon"></span></strong></li>
@@ -27,14 +27,12 @@
<div class="col-md-6">
<h4><strong>Connection Details</strong></h4>
<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>
<li>Organization: <strong><span id="organization"></span></strong></li>
</ul>
</div>
</div>
<div class="modal-footer">
<span class="text-muted">Service provided by ip-api.com.</span>
<span class="text-muted">Telize service written by <a href="https://github.com/fcambus/telize" target="_blank">Frederic Cambus</a>.</span>
</div>
</div>
</div>
@@ -43,7 +41,7 @@
<script>
function getUserLocation(ip_address) {
$.ajax({
url: 'http://ip-api.com/json/' + ip_address,
url: 'https://telize.myhtpc.co.za/geoip/' + ip_address,
cache: true,
async: true,
type: 'GET',
@@ -55,13 +53,11 @@
$('#modal_header_ip_address').html('<i class="fa fa-map-marker"></i> IP Address: ' + ip_address);
$('#country').html(data.country);
$('#city').html(data.city);
$('#region').html(data.regionName);
$('#region').html(data.region);
$('#timezone').html(data.timezone);
$('#lat').html(data.lat);
$('#lon').html(data.lon);
$('#isp').html(data.isp);
$('#org').html(data.org);
$('#as').html(data.as);
$('#lat').html(data.latitude);
$('#lon').html(data.longitude);
$('#organization').html(data.organization);
},
timeout: 5000
});

View File

@@ -176,7 +176,9 @@ function getPlatformImagePath(platformName) {
if (platformName.indexOf("Roku") > -1) {
return 'interfaces/default/images/platforms/roku.png';
} else if (platformName.indexOf("Apple TV") > -1) {
return 'interfaces/default/images/platforms/appletv.png';
return 'interfaces/default/images/platforms/atv.png';
} else if (platformName.indexOf("tvOS") > -1) {
return 'interfaces/default/images/platforms/atv.png';
} else if (platformName.indexOf("Firefox") > -1) {
return 'interfaces/default/images/platforms/firefox.png';
} else if (platformName.indexOf("Chromecast") > -1) {
@@ -201,6 +203,8 @@ function getPlatformImagePath(platformName) {
return 'interfaces/default/images/platforms/safari.png';
} else if (platformName.indexOf("Internet Explorer") > -1) {
return 'interfaces/default/images/platforms/ie.png';
} else if (platformName.indexOf("Microsoft Edge") > -1) {
return 'interfaces/default/images/platforms/msedge.png';
} else if (platformName.indexOf("Unknown Browser") > -1) {
return 'interfaces/default/images/platforms/dafault.png';
} else if (platformName.indexOf("Windows-XBMC") > -1) {
@@ -223,6 +227,8 @@ function getPlatformImagePath(platformName) {
return 'interfaces/default/images/platforms/win8.png';
} else if (platformName.indexOf("Windows phone") > -1) {
return 'interfaces/default/images/platforms/wp.png';
} else if (platformName.indexOf("Plex Media Player") > -1) {
return 'interfaces/default/images/platforms/pmp.png';
} else {
return 'interfaces/default/images/platforms/default.png';
}
@@ -230,7 +236,9 @@ function getPlatformImagePath(platformName) {
function isPrivateIP(ip_address) {
if (ip_address.indexOf(".") > -1) {
var parts = ip_address.split('.');
// get IPv4 mapped address (xxx.xxx.xxx.xxx) from IPv6 addresss (::ffff:xxx.xxx.xxx.xxx)
var parts = ip_address.split(":");
var parts = parts[parts.length - 1].split('.');
if (parts[0] === '10' ||
(parts[0] === '172' && (parseInt(parts[1], 10) >= 16 && parseInt(parts[1], 10) <= 31)) ||
(parts[0] === '192' && parts[1] === '168')) {

View File

@@ -18,7 +18,7 @@ history_table_options = {
"lengthMenu":"Show _MENU_ entries per page",
"info":"Showing _START_ to _END_ of _TOTAL_ history items",
"infoEmpty":"Showing 0 to 0 of 0 entries",
"infoFiltered":"(filtered from _MAX_ total entries)",
"infoFiltered":"<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
"emptyTable": "No data in table"
},
"pagingType": "bootstrap",
@@ -265,6 +265,9 @@ history_table_options = {
createChildTable(this, rowData)
}
});
$("#history_table_info").append('<span class="hidden-md hidden-sm hidden-xs"> with a duration of ' + settings.json.filter_duration +
' (filtered from ' + settings.json.total_duration + ' total)</span>');
},
"preDrawCallback": function(settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";

View File

@@ -1,3 +1,4 @@
var users_to_delete = [];
var users_to_purge = [];
users_list_table_options = {
@@ -22,7 +23,8 @@ users_list_table_options = {
"targets": [0],
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<div class="edit-user-toggles"><button class="btn btn-xs btn-warning" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&nbsp' +
$(td).html('<div class="edit-user-toggles"><button class="btn btn-xs btn-warning delete-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button>&nbsp' +
'<button class="btn btn-xs btn-warning purge-user" 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
@@ -286,16 +288,44 @@ $('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > inpu
});
});
$('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > button', function () {
$('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > button.delete-user', 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) {
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
if (index_delete === -1) {
users_to_delete.push(rowData['user_id']);
if (index_purge === -1) {
tr.find('button.purge-user').click();
}
} else {
users_to_delete.splice(index_delete, 1);
if (index_purge != -1) {
tr.find('button.purge-user').click();
}
}
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
});
$('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > button.purge-user', function () {
var tr = $(this).parents('tr');
var row = users_list_table.row(tr);
var rowData = row.data();
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
if (index_purge === -1) {
users_to_purge.push(rowData['user_id']);
} else {
users_to_purge.splice(index, 1);
users_to_purge.splice(index_purge, 1);
if (index_delete != -1) {
tr.find('button.delete-user').click();
}
}
$(this).toggleClass('btn-warning').toggleClass('btn-danger');
});

View File

@@ -6,7 +6,7 @@ from plexpy import helpers
<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-config-modal-header">Set Config</h4>
<h4 class="modal-title" id="notification-config-modal-header">${agent['name']} Settings</h4>
</div>
<div class="modal-body">
<div class="container-fluid">
@@ -25,7 +25,7 @@ from plexpy import helpers
% endif
</div>
</div>
<p class="help-block">${item['description']}</p>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'button':
<div class="form-group">
@@ -34,14 +34,14 @@ from plexpy import helpers
<input type="${item['input_type']}" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</div>
</div>
<p class="help-block">${item['description']}</p>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'checkbox':
<div class="checkbox">
<label>
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${helpers.checked(item['value'])}> ${item['label']}
</label>
<p class="help-block">${item['description']}</p>
<p class="help-block">${item['description'] | n}</p>
<input type="hidden" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</div>
% elif item['input_type'] == 'select':
@@ -60,7 +60,7 @@ from plexpy import helpers
</select>
</div>
</div>
<p class="help-block">${item['description']}</p>
<p class="help-block">${item['description'] | n}</p>
</div>
% endif
% endfor
@@ -84,24 +84,29 @@ from plexpy import helpers
% endif
<script>
$('#osxnotifyregister').click(function () {
var osx_notify_app = $("#osx_notify_app").val();
$.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
})
$('#save-notification-item').click(function() {
doAjaxCall('set_notification_config', $(this), 'tabs', true);
// Reload modal to update certain fields
function reloadModal() {
$.ajax({
url: 'get_notification_agent_config',
data: { config_id: '${config_id}' },
data: { config_id: '${agent["id"]}' },
cache: false,
async: true,
complete: function (xhr, status) {
$("#notification-config-modal").html(xhr.responseText);
}
});
}
$('#osxnotifyregister').click(function () {
var osx_notify_app = $("#osx_notify_app").val();
$.get("/osxnotifyregister", { 'app': osx_notify_app }, function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
})
$('#save-notification-item').click(function () {
doAjaxCall('set_notification_config', $(this), 'tabs', true);
// Reload modal to update certain fields
reloadModal();
return false;
});
@@ -121,8 +126,20 @@ from plexpy import helpers
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#testIFTTT').click(function () {
$.get("/test_ifttt",
function (data) { $('#ajaxMsg').html("<i class='fa fa-check'></i> " + data); });
$('#ajaxMsg').addClass('success').fadeIn().delay(3000).fadeOut();
});
$('#pushbullet_apikey').on('change', function () {
doAjaxCall('set_notification_config', $(this), 'tabs', true);
reloadModal();
return false;
});
// Never send checkbox values directly, always substitute value in hidden input.
$('.checkboxes').click(function() {
$('.checkboxes').click(function () {
var configToggle = $(this).data('id');
if ($(this).is(":checked")) {
$("#"+configToggle).val(1);

View File

@@ -57,6 +57,41 @@ from plexpy import helpers
</label>
<p class="help-block">Trigger notification when a media item triggers the defined buffer threshold.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_created" ${helpers.checked(data['on_created'])} class="toggle-switches">
Notify on recently added
</label>
<p class="help-block">Trigger notification when a media item is added to the Plex Media Server.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extdown" ${helpers.checked(data['on_extdown'])} class="toggle-switches">
Notify on Plex remote access down
</label>
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached externally.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intdown" ${helpers.checked(data['on_intdown'])} class="toggle-switches">
Notify on Plex server down
</label>
<p class="help-block">Trigger notification when the Plex Media Server cannot be reached internally.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_extup" ${helpers.checked(data['on_extup'])} class="toggle-switches">
Notify on Plex remote access back up
</label>
<p class="help-block">Trigger notification when the Plex Media Server can be reached externally after being down.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" data-size="small" data-id="${data['id']}" data-config-name="${data['config_prefix']}_on_intup" ${helpers.checked(data['on_intup'])} class="toggle-switches">
Notify on Plex server back up
</label>
<p class="help-block">Trigger notification when the Plex Media Server can be reached internally after being down.</p>
</div>
</div>
</div>
</div>
@@ -81,6 +116,7 @@ from plexpy import helpers
console.log('success');
}
});
$('.toggle-notification-triggers-modal[data-id=' + configToggle + ']').addClass('active');
} else {
var data = {};
data[$(this).data('config-name')] = 0;
@@ -92,6 +128,9 @@ from plexpy import helpers
console.log('success');
}
});
if (!($('.toggle-switches').is(":checked"))) {
$('.toggle-notification-triggers-modal[data-id=' + configToggle + ']').removeClass('active');
}
}
});
</script>

View File

@@ -11,13 +11,13 @@ data[array_index] :: Usable parameters
== Global keys ==
rating_key Returns the unique identifier for the media item.
type Returns the type of media. Either 'movie' or 'season'.
media_type Returns the media type of media. Either 'movie' or 'season' or 'album'.
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' ==
== Only if 'media_type' is 'movie' ==
year Returns the movie release year.
DOCUMENTATION :: END
@@ -29,7 +29,7 @@ DOCUMENTATION :: END
% for item in data:
<div class="dashboard-recent-media-instance">
<li>
% if item['type'] == 'season' or item['type'] == 'movie':
% if item['media_type'] == 'season' or item['media_type'] == 'movie':
<a href="info?item_id=${item['rating_key']}">
<div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
@@ -43,16 +43,16 @@ DOCUMENTATION :: END
</div>
</div>
<div class="dashboard-recent-media-metacontainer">
% if item['type'] == 'season':
% if item['media_type'] == 'season':
<h3 title="${item['parent_title']}">${item['parent_title']}</h3>
<h3 class="text-muted">${item['title']}</h3>
% elif item['type'] == 'movie':
% elif item['media_type'] == 'movie':
<h3 title="${item['title']}">${item['title']}</h3>
<h3 class="text-muted">${item['year']}</h3>
% endif
</div>
</a>
% elif item['type'] == 'album':
% elif item['media_type'] == 'album':
<a href="info?item_id=${item['rating_key']}">
<div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);">

View File

@@ -26,13 +26,13 @@
<%def name="javascriptIncludes()">
<script>
$('#search_button').removeClass('btn-inactive');
$('#search_query').val('${query}').css({ right: '0', width: '250px' }).addClass('active');
$('#query').val("${query.replace('"','\\"')}").css({ right: '0', width: '250px' }).addClass('active');
$.ajax({
url: 'get_search_results_children',
type: "GET",
async: true,
data: {'query': "${query}"},
data: {'query': "${query.replace('"','\\"')}"},
complete: function (xhr, status) {
$("#search-results-list").html(xhr.responseText);
}

View File

@@ -3,7 +3,7 @@
import plexpy
from plexpy import notifiers, common, versioncheck
available_notification_agents = notifiers.available_notification_agents()
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['name'])
%>
<%def name="headIncludes()">
</%def>
@@ -86,9 +86,9 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Table History
<input type="checkbox" id="group_history_tables" name="group_history_tables" value="1" ${config['group_history_tables']}> Group Table and Watch Statistics History
</label>
<p class="help-block">Group successive play history by the same user as a single entry in tables.</p>
<p class="help-block">Group successive play history by the same user as a single entry in the tables and watch statistics.</p>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
</div>
@@ -109,9 +109,10 @@ available_notification_agents = notifiers.available_notification_agents()
<option id="card-popular_movies" value="popular_movies">Most Popular Movie</option>
<option id="card-top_music" value="top_music">Most Listened Music</option>
<option id="card-popular_music" value="popular_music">Most Popular Music</option>
<option id="card-last_watched" value="last_watched">Last Watched</option>
<option id="card-top_users" value="top_users">Most Active User</option>
<option id="card-top_platforms" value="top_platforms">Most Active Platform</option>
<option id="card-last_watched" value="last_watched">Last Watched</option>
<option id="card-most_concurrent" value="most_concurrent">Most Concurrent Streams</option>
</select>
</div>
</div>
@@ -273,9 +274,10 @@ available_notification_agents = notifiers.available_notification_agents()
<label for="pms_ip">Plex IP or Hostname</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="pms-settings form-control" id="pms_ip" name="pms_ip" value="${config['pms_ip']}" size="30" data-parsley-trigger="change" aria-describedby="server-verified" required>
<input type="text" class="pms-settings form-control" id="pms_ip" name="pms_ip" value="${config['pms_ip']}" size="30" data-parsley-trigger="change" aria-describedby="server-verified" data-parsley-errors-container="#pms_ip_error" required>
<span class="form-control-feedback" id="pms-verify" aria-hidden="true" style="display: none;"></span>
</div>
<div id="pms_ip_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">IP Address or hostname for Plex Media Server.</p>
</div>
@@ -293,7 +295,7 @@ available_notification_agents = notifiers.available_notification_agents()
<label>
<input type="checkbox" id="pms_is_remote" name="pms_is_remote" value="1" ${config['pms_is_remote']}> Remote Server
</label>
<p class="help-block">Check this is your Plex Server is not on the same local network as PlexPy.</p>
<p class="help-block">Check this if your Plex Server is not on the same local network as PlexPy.</p>
</div>
<div class="checkbox">
<label>
@@ -312,7 +314,8 @@ available_notification_agents = notifiers.available_notification_agents()
<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 complete folder path where your Plex Server logs are, shortcuts are not recognized.<br /><a href="https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files" target="_blank">Click here</a> for help. This is required if you enable IP logging. </p>
<p class="help-block">Set the complete folder path where your Plex Server logs are, shortcuts are not recognized.<br />
<a href="https://support.plex.tv/hc/en-us/articles/200250417-Plex-Media-Server-Log-Files" target="_blank">Click here</a> for help. This is required if you enable IP logging (for PMS 0.9.12 and below). </p>
</div>
<input type="hidden" id="pms_identifier" name="pms_identifier" value="${config['pms_identifier']}">
@@ -403,21 +406,31 @@ available_notification_agents = notifiers.available_notification_agents()
</label>
<p class="help-block">Instead of polling the server at regular intervals let the server tell us when something happens. This is currently experimental. Encrypted websocket is not currently supported.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
</label>
<p class="help-block">Enable to have PlexPy check if remote access to the Plex Media Server goes down. Your server needs to have remote access enabled.</p>
</div>
<div class="padded-header">
<h3>History Logging</h3>
</div>
<p class="help-block">Keep records of all movie, TV show, or music items played from your Plex Media Server.</p>
<div class="checkbox">
<label>
<input type="checkbox" id="video_logging_enable" name="video_logging_enable" value="1" ${config['video_logging_enable']}> Log Movies and TV
<input type="checkbox" id="movie_logging_enable" name="movie_logging_enable" value="1" ${config['movie_logging_enable']}> Enable Movie Logging
</label>
<p class="help-block">Keep records of all video items played from your Plex Media Server.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Log Music
<input type="checkbox" id="tv_logging_enable" name="tv_logging_enable" value="1" ${config['tv_logging_enable']}> Enable TV Show Logging
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Enable Music Logging
</label>
<p class="help-block">Keep records of all audio items played from your Plex Media Server. VERY experimental.</p>
</div>
<div class="form-group">
<label for="logging_ignore_interval">Ignore Interval</label>
@@ -435,7 +448,7 @@ available_notification_agents = notifiers.available_notification_agents()
</label>
<span id="debugLogCheck" style="color: #eb8600; padding-left: 10px;"></span>
<p class="help-block">
Enable this to attempt to log the IP address of the user.
Enable this to attempt to log the IP address of the user (for PMS 0.9.12 and below, IP address is automatically logged for PMS 0.9.14 and above).
</p>
</div>
@@ -473,7 +486,12 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable Movie and TV Notifications
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable Movie Notifications
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="tv_notify_enable" id="tv_notify_enable" value="1" ${config['tv_notify_enable']}> Enable TV Show Notifications
</label>
</div>
<div class="checkbox">
@@ -481,9 +499,14 @@ available_notification_agents = notifiers.available_notification_agents()
<input type="checkbox" name="music_notify_enable" id="music_notify_enable" value="1" ${config['music_notify_enable']}> Enable Music Notifications
</label>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="notify_recently_added" id="notify_recently_added" value="1" ${config['notify_recently_added']}> Enable Recently Added Notifications
</label>
</div>
<div class="padded-header">
<h3>Notification Tuning</h3>
<h3>Current Activity Notifications</h3>
</div>
<div class="form-group">
@@ -503,6 +526,28 @@ available_notification_agents = notifiers.available_notification_agents()
<p class="help-block">Disable to prevent consecutive notifications (i.e. both watched &amp; stopped notifications).</p>
</div>
<div class="padded-header">
<h3>Recently Added Notifications</h3>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="notify_recently_added_grandparent" id="notify_recently_added_grandparent" value="1" ${config['notify_recently_added_grandparent']}> Group notifications for recently added TV Shows or Music
</label>
<p class="help-block">Enable to only get one TV Show or Artist notification for a batch of recently added Episodes or Tracks. Movies are unaffected.<br />
Note: No Season/Episode or Album/Track metadata will be available.</p>
</div>
<div class="form-group">
<label for="notify_recently_added_delay">Notification Delay</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_recently_added_delay" name="notify_recently_added_delay" value="${config['notify_recently_added_delay']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_recently_added_delay_error" required>
</div>
<div id="notify_recently_added_delay_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Set the delay for recently added notifications to allow metadata to be processed. Minimum 60 seconds.</p>
</div>
<div class="padded-header">
<h3>Custom Notification Messages</h3>
</div>
@@ -515,9 +560,9 @@ available_notification_agents = notifiers.available_notification_agents()
<a href="#notify-text-tags-modal" data-toggle="modal">here</a> to view usage information.
</p>
<br/>
<ul id="accordion" class="accordion list-unstyled">
<ul id="accordion-session" class="accordion list-unstyled">
<li>
<div class="link"><i class="fa fa-play"></i>Playback Start<i class="fa fa-chevron-down"></i></div>
<div class="link"><i class="fa fa-play fa-fw"></i>&nbsp;Playback Start<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
@@ -527,14 +572,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<div class="form-group">
<label for="notify_on_start_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_start_body_text" name="notify_on_start_body_text" value="${config['notify_on_start_body_text']}" data-parsley-trigger="change" required>
<textarea class="form-control" id="notify_on_start_body_text" name="notify_on_start_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_start_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-stop"></i>Playback Stop<i class="fa fa-chevron-down"></i></div>
<div class="link"><i class="fa fa-stop fa-fw"></i>&nbsp;Playback Stop<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
@@ -544,14 +589,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<div class="form-group">
<label for="notify_on_stop_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_stop_body_text" name="notify_on_stop_body_text" value="${config['notify_on_stop_body_text']}" data-parsley-trigger="change" required>
<textarea class="form-control" id="notify_on_stop_body_text" name="notify_on_stop_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_stop_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-pause"></i>Playback Pause<i class="fa fa-chevron-down"></i></div>
<div class="link"><i class="fa fa-pause fa-fw"></i>&nbsp;Playback Pause<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
@@ -561,14 +606,14 @@ available_notification_agents = notifiers.available_notification_agents()
</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>
<textarea class="form-control" id="notify_on_pause_body_text" name="notify_on_pause_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_pause_body_text']}</textarea>
<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>
<div class="link"><i class="fa fa-play fa-fw"></i>&nbsp;Playback Resume<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
@@ -578,14 +623,14 @@ available_notification_agents = notifiers.available_notification_agents()
</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>
<textarea class="form-control" id="notify_on_resume_body_text" name="notify_on_resume_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_resume_body_text']}</textarea>
<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>
<div class="link"><i class="fa fa-eye fa-fw"></i>&nbsp;Watched<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
@@ -595,14 +640,14 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<div class="form-group">
<label for="notify_on_watched_body_text">Message Body</label>
<input class="form-control" type="text" id="notify_on_watched_body_text" name="notify_on_watched_body_text" value="${config['notify_on_watched_body_text']}" data-parsley-trigger="change" required>
<textarea class="form-control" id="notify_on_watched_body_text" name="notify_on_watched_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_watched_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-spinner"></i>Buffer Warnings<i class="fa fa-chevron-down"></i></div>
<div class="link"><i class="fa fa-spinner fa-fw"></i>&nbsp;Buffer Warnings<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
@@ -612,7 +657,94 @@ available_notification_agents = notifiers.available_notification_agents()
</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>
<textarea class="form-control" id="notify_on_buffer_body_text" name="notify_on_buffer_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_buffer_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
</ul>
<ul id="accordion-timeline" class="accordion list-unstyled">
<li>
<div class="link"><i class="fa fa-download fa-fw"></i>&nbsp;Recently Added<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_created_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_created_subject_text" name="notify_on_created_subject_text" value="${config['notify_on_created_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_created_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_created_body_text" name="notify_on_created_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_created_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-server fa-fw"></i>&nbsp;Plex Remote Access Down<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_extdown_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_extdown_subject_text" name="notify_on_extdown_subject_text" value="${config['notify_on_extdown_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_extdown_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_extdown_body_text" name="notify_on_extdown_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extdown_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-server fa-fw"></i>&nbsp;Plex Server Down<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_intdown_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_intdown_subject_text" name="notify_on_intdown_subject_text" value="${config['notify_on_intdown_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_intdown_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_intdown_body_text" name="notify_on_intdown_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_intdown_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-server fa-fw"></i>&nbsp;Plex Remote Access Back Up<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_extup_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_extup_subject_text" name="notify_on_extup_subject_text" value="${config['notify_on_extup_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_extup_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_extup_body_text" name="notify_on_extup_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_extup_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
</ul>
</li>
<li>
<div class="link"><i class="fa fa-server fa-fw"></i>&nbsp;Plex Server Back Up<i class="fa fa-chevron-down"></i></div>
<ul class="submenu">
<li>
<div class="form-group">
<label for="notify_on_intup_subject_text">Subject Line</label>
<input class="form-control" type="text" id="notify_on_intup_subject_text" name="notify_on_intup_subject_text" value="${config['notify_on_intup_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_intup_body_text">Message Body</label>
<textarea class="form-control" id="notify_on_intup_body_text" name="notify_on_intup_body_text" data-parsley-trigger="change" data-autoresize required>${config['notify_on_intup_body_text']}</textarea>
<p class="help-block">Set a custom body.</p>
</div>
</li>
@@ -635,7 +767,7 @@ available_notification_agents = notifiers.available_notification_agents()
% for agent in available_notification_agents:
<li>
<span>
% if agent['on_play'] or agent['on_stop'] or agent['on_pause'] or agent['on_resume'] or agent['on_buffer'] or agent['on_watched']:
% if agent['on_play'] or agent['on_stop'] or agent['on_pause'] or agent['on_resume'] or agent['on_buffer'] or agent['on_watched'] or agent['on_created'] or agent['on_extdown'] or agent['on_intdown']:
<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>
@@ -832,115 +964,248 @@ available_notification_agents = notifiers.available_notification_agents()
<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">Notification string substitutions</h4>
<h4 class="modal-title">Notification String Substitutions</h4>
</div>
<div class="modal-body">
<div>
<p class="help-block">
If a value for a selected parameter cannot be provided nothing will be outputted for it.
If the value for a selected parameter cannot be provided, it will display as blank.
</p>
<table>
<table class="notification-params">
<thead>
<tr>
<th>
Server Details
</th>
</tr>
</thead>
<tbody>
<tr>
<td width="150"><strong>{server_name}</strong></td>
<td><strong>{server_name}</strong></td>
<td>The name of your Plex Server.</td>
</tr>
<tr>
<td width="150"><strong>{user}</strong></td>
<td><strong>{server_uptime}</strong></td>
<td>The uptime (in days, hours, mins, secs) of your Plex Server.</td>
</tr>
</tbody>
</table>
<table class="notification-params">
<thead>
<tr>
<th>
Stream Details
</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>{user}</strong></td>
<td>The username of the person streaming.</td>
</tr>
<tr>
<td width="150"><strong>{platform}</strong></td>
<td><strong>{platform}</strong></td>
<td>The type of client being used for playback.</td>
</tr>
<tr>
<td width="150"><strong>{player}</strong></td>
<td><strong>{player}</strong></td>
<td>The name of the device being used for playback.</td>
</tr>
<tr>
<td width="150"><strong>{media_type}</strong></td>
<td><strong>{ip_address}</strong></td>
<td>The IP address of the device being used for playback. (PMS 0.9.14 and above)</td>
</tr>
<tr>
<td><strong>{media_type}</strong></td>
<td>The type of media being played (movie, episode, track).</td>
</tr>
<tr>
<td width="150"><strong>{title}</strong></td>
<td>The title of the item being played.</td>
</tr>
<tr>
<td width="150"><strong>{show_name}</strong></td>
<td>The title of the TV series being played.</td>
</tr>
<tr>
<td width="150"><strong>{episode_name}</strong></td>
<td>The title of the episode being played.</td>
</tr>
<tr>
<td width="150"><strong>{artist_name}</strong></td>
<td>The name of the artist being played.</td>
</tr>
<tr>
<td width="150"><strong>{album_name}</strong></td>
<td>The title of the album being played.</td>
</tr>
<tr>
<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>{transcode_decision}</strong></td>
<td>The transcode decisions for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{year}</strong></td>
<td>The release year for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{studio}</strong></td>
<td>The studio for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{content_rating}</strong></td>
<td>The content rating for the media item. (e.g. TV-MA, TV-PG, etc.)</td>
</tr>
<tr>
<td width="150"><strong>{summary}</strong></td>
<td>A short plot summary for the media item.</td>
</tr>
<tr>
<td width="150"><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</td>
</tr>
<tr>
<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><strong>{stream_duration}</strong></td>
<td>The stream duration (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{remaining_duration}</strong></td>
<td><strong>{remaining_duration}</strong></td>
<td>The remaining duration (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{progress}</strong></td>
<td><strong>{progress}</strong></td>
<td>The last reported offset (in minutes) for the item.</td>
</tr>
<tr>
<td width="150"><strong>{progress_percent}</strong></td>
<td><strong>{progress_percent}</strong></td>
<td>The last reported progress percent for the item.</td>
</tr>
<tr>
<td><strong>{container}</strong></td>
<td>The media container of the original media.</td>
</tr>
<tr>
<td><strong>{video_codec}</strong></td>
<td>The video codec of the original media.</td>
</tr>
<tr>
<td><strong>{video_bitrate}</strong></td>
<td>The video bitrate of the original media.</td>
</tr>
<tr>
<td><strong>{video_width}</strong></td>
<td>The video width of the original media.</td>
</tr>
<tr>
<td><strong>{video_height}</strong></td>
<td>The video height of the original media.</td>
</tr>
<tr>
<td><strong>{video_resolution}</strong></td>
<td>The video resolution of the original media.</td>
</tr>
<tr>
<td><strong>{video_framerate}</strong></td>
<td>The video framerate of the original media.</td>
</tr>
<tr>
<td><strong>{aspect_ratio}</strong></td>
<td>The aspect ratio of the original media.</td>
</tr>
<tr>
<td><strong>{audio_codec}</strong></td>
<td>The audio codec of the original media.</td>
</tr>
<tr>
<td><strong>{audio_channels}</strong></td>
<td>The audio channels of the original media.</td>
</tr>
<tr>
<td><strong>{transcode_decision}</strong></td>
<td>The stream transcode decisions for the media item.</td>
</tr>
<tr>
<td><strong>{video_decision}</strong></td>
<td>The video transcode decisions for the media item.</td>
</tr>
<tr>
<td><strong>{audio_decision}</strong></td>
<td>The audio transcode decisions for the media item.</td>
</tr>
<tr>
<td><strong>{transcode_container}</strong></td>
<td>The media container of the transcoded media.</td>
</tr>
<tr>
<td><strong>{transcode_video_codec}</strong></td>
<td>The video codec of the transcoded media.</td>
</tr>
<tr>
<td><strong>{transcode_video_width}</strong></td>
<td>The video width of the transcoded media.</td>
</tr>
<tr>
<td><strong>{transcode_video_height}</strong></td>
<td>The video height of the transcoded media.</td>
</tr>
<tr>
<td><strong>{transcode_audio_codec}</strong></td>
<td>The audio codec of the transcoded media.</td>
</tr>
<tr>
<td><strong>{transcode_audio_channels}</strong></td>
<td>The audio channels of the transcoded media.</td>
</tr>
</tbody>
</table>
<table class="notification-params">
<thead>
<tr>
<th>
Metadata Details
</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>{title}</strong></td>
<td>The full title of the item being played.</td>
</tr>
<tr>
<td><strong>{show_name}</strong></td>
<td>The title of the TV series being played.</td>
</tr>
<tr>
<td><strong>{episode_name}</strong></td>
<td>The title of the episode being played.</td>
</tr>
<tr>
<td><strong>{artist_name}</strong></td>
<td>The name of the artist being played.</td>
</tr>
<tr>
<td><strong>{album_name}</strong></td>
<td>The title of the album being played.</td>
</tr>
<tr>
<td><strong>{track_name}</strong></td>
<td>The title of the track being played.</td>
</tr>
<tr>
<td><strong>{season_num}</strong></td>
<td>The season number for the media item if item is episode.</td>
</tr>
<tr>
<td><strong>{season_num00}</strong></td>
<td>The two digit season number.</td>
</tr>
<tr>
<td><strong>{episode_num}</strong></td>
<td>The episode number for the media item if item is episode.</td>
</tr>
<tr>
<td><strong>{episode_num00}</strong></td>
<td>The two digit episode number.</td>
</tr>
<tr>
<td><strong>{year}</strong></td>
<td>The release year for the media item.</td>
</tr>
<tr>
<td><strong>{studio}</strong></td>
<td>The studio for the media item.</td>
</tr>
<tr>
<td><strong>{content_rating}</strong></td>
<td>The content rating for the media item. (e.g. TV-MA, TV-PG, etc.)</td>
</tr>
<tr>
<td><strong>{directors}</strong></td>
<td>A list of directors for the media item.</td>
</tr>
<tr>
<td><strong>{writers}</strong></td>
<td>A list of writers for the media item.</td>
</tr>
<tr>
<td><strong>{actors}</strong></td>
<td>A list of actors for the media item.</td>
</tr>
<tr>
<td><strong>{genres}</strong></td>
<td>A list of genres for the media item.</td>
</tr>
<tr>
<td><strong>{summary}</strong></td>
<td>A short plot summary for the media item.</td>
</tr>
<tr>
<td><strong>{tagline}</strong></td>
<td>A tagline for the media item.</td>
</tr>
<tr>
<td><strong>{rating}</strong></td>
<td>The rating (out of 10) for the item.</td>
</tr>
<tr>
<td><strong>{duration}</strong></td>
<td>The duration (in minutes) for the item.</td>
</tr>
</tbody>
</table>
</div>
@@ -954,7 +1219,7 @@ available_notification_agents = notifiers.available_notification_agents()
<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">Notification exclusion tags</h4>
<h4 class="modal-title">Notification Exclusion Tags</h4>
</div>
<div class="modal-body">
<div>
@@ -963,21 +1228,24 @@ available_notification_agents = notifiers.available_notification_agents()
</div>
<div>
<p class="help-block">All text inside a <strong>movie</strong> tag will only be sent when the media item being played back is a movie.</p>
<pre>Example: {user} has started playing {title} &lt;movie&gt;({year})&lt;/movie&gt;</pre>
<p><strong style="color: #fff;">Example:</strong></p>
<pre>{user} has started playing {title} &lt;movie&gt;({year})&lt;/movie&gt;</pre>
</div>
<div class="wellheader">
<h4>TV Tag <strong>&lt;tv&gt;&lt;/tv&gt;</strong></h4>
</div>
<div>
<p class="help-block">All text inside a <strong>tv</strong> tag will only be sent when the media item being played back is an episode.</p>
<pre>Example: {user} has started playing {title} &lt;tv&gt;(S{season_num}E{episode_num})&lt;/tv&gt;</pre>
<p><strong style="color: #fff;">Example:</strong></p>
<pre>{user} has started playing {title} &lt;tv&gt;(S{season_num}E{episode_num})&lt;/tv&gt;</pre>
</div>
<div class="wellheader">
<h4>Music Tag <strong>&lt;music&gt;&lt;/music&gt;</strong></h4>
</div>
<div>
<p class="help-block">All text inside a <strong>music</strong> tag will only be sent when the media item being played back is a music track.</p>
<pre>Example: {user} has started playing {title} &lt;music&gt;(Track {episode_num})&lt;/music&gt;</pre>
<p><strong style="color: #fff;">Example:</strong></p>
<pre>{user} has started playing {title} &lt;music&gt;(Track {episode_num})&lt;/music&gt;</pre>
</div>
</div>
</div>
@@ -994,7 +1262,7 @@ available_notification_agents = notifiers.available_notification_agents()
<h4 class="modal-title">Changelog</h4>
</div>
<div class="modal-body">
${versioncheck.read_changelog()}
${versioncheck.read_changelog() | n}
</div>
<div class="modal-footer">
</div>
@@ -1051,17 +1319,21 @@ $(document).ready(function() {
}
var configForm = $("#configUpdate");
function saveSettings() {
if (configForm.parsley().validate()) {
doAjaxCall('configUpdate', $(this), 'tabs', true);
postSaveChecks();
return false;
} else {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspPlease verify your settings.', false, true, 2000, true)
}
}
$('.save-button').click(function() {
if ($("#pms_identifier").val() == "") {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspPlease verify your server.',false,true,2000,true)
verifyServer(function () { saveSettings() });
} else {
if (configForm.parsley().validate()) {
doAjaxCall('configUpdate',$(this),'tabs',true);
postSaveChecks();
return false;
} else {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspPlease verify your settings.',false,true,2000,true)
}
saveSettings();
}
});
@@ -1147,7 +1419,7 @@ $(document).ready(function() {
verifyServer();
});
function verifyServer() {
function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val()
var pms_port = $("#pms_port").val()
if (($("#pms_ip").val() !== '') || ($("#pms_port").val() !== '')) {
@@ -1170,10 +1442,15 @@ $(document).ready(function() {
$("#pms-verify").html('<i class="fa fa-check"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").removeClass("has-error");
if (_callback) {
_callback();
}
} else {
$("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspCould not verify your server.', false, true, 2000, true)
}
}
});
@@ -1181,6 +1458,7 @@ $(document).ready(function() {
$("#pms-verify").html('<i class="fa fa-close"></i>');
$('#pms-verify').fadeIn('fast');
$("#pms-ip-group").addClass("has-error");
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspCould not verify your server.', false, true, 2000, true)
}
}
@@ -1199,7 +1477,7 @@ $(document).ready(function() {
'X-Plex-Version': '${common.VERSION_NUMBER}',
'X-Plex-Platform': '${common.PLATFORM}',
'X-Plex-Platform-Version': '${common.PLATFORM_VERSION}',
'X-Plex-Client-Identifier': '${config['pms_uuid']}',
'X-Plex-Client-Identifier': '${config["pms_uuid"]}',
'Authorization': 'Basic ' + btoa($("#pms_username").val() + ':' + $("#pms_password").val())
},
error: function(jqXHR, textStatus, errorThrown) {
@@ -1292,7 +1570,8 @@ $(document).ready(function() {
}
}
var accordion = new Accordion($('#accordion'), false);
var accordion_session = new Accordion($('#accordion-session'), false);
var accordion_timeline = new Accordion($('#accordion-timeline'), false);
var cards = "${config['home_stats_cards']}".split(/[\s,]+/);
cards.forEach(function (item) {
@@ -1334,6 +1613,14 @@ $(document).ready(function() {
e.preventDefault()
});
// auto resizing textarea for custom notification message body
$('textarea[data-autoresize]').each(function() {
var offset = this.offsetHeight - this.clientHeight;
var resizeTextarea = function(el) {
$(el).css('height', 'auto').css('height', el.scrollHeight + offset);
};
$(this).on('focus keyup input', function() { resizeTextarea(this); }).removeAttr('data-autoresize');
});
});
</script>
</%def>
</%def>

View File

@@ -262,33 +262,6 @@ 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']:
@@ -297,13 +270,15 @@ from plexpy import helpers
var user_id = null;
% endif
var username = '${data['username'].replace("'", "\\'")}';
$("#edit-user-tooltip").tooltip();
// Populate watch time stats
$.ajax({
url: 'get_user_watch_time_stats',
async: true,
data: { user_id: user_id, user: '${data['username']}' },
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-time-stats").html(xhr.responseText);
}
@@ -313,21 +288,23 @@ from plexpy import helpers
$.ajax({
url: 'get_user_player_stats',
async: true,
data: { user_id: user_id, user: '${data['username']}' },
data: { user_id: user_id, user: username },
complete: function(xhr, status) {
$("#user-player-stats").html(xhr.responseText);
}
});
$( "#history-tab-btn" ).one( "click", function() {
function loadHistoryTable(media_type) {
// Build watch history table
history_table_options.ajax = {
"url": "get_history",
url: 'get_history',
type: 'post',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'user_id': user_id,
'user': "${data['username']}"
return {
'json_data': JSON.stringify( d ),
'user_id': user_id,
'user': username,
'media_type': media_type
};
}
}
@@ -338,6 +315,34 @@ from plexpy import helpers
$(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table', history_table);
$('#history_table_filter').prepend('<div class="btn-group" data-toggle="buttons" id="media_type-selection" style="padding-right: 15px;"> \
<label class="btn btn-dark active"> \
<input type="radio" name="media_type-filter" id="history-all" value="all" autocomplete="off"> All \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-movies" value="movie" autocomplete="off"> Movies \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-tv_shows" value="episode" autocomplete="off"> TV Shows \
</label> \
<label class="btn btn-dark"> \
<input type="radio" name="media_type-filter" id="history-music" value="track" autocomplete="off"> Music \
</label> \
</div>');
$('#media_type-selection').on('change', function () {
$('#media_type-selection > label').removeClass('active');
selected_filter = $('input[name=media_type-filter]:checked', '#media_type-selection');
$(selected_filter).closest('label').addClass('active');
media_type = $(selected_filter).val();
history_table.draw();
});
}
$( "#history-tab-btn" ).one( "click", function() {
var media_type = 'all';
loadHistoryTable(media_type);
});
$( "#ip-tab-btn" ).one( "click", function() {
@@ -348,7 +353,7 @@ from plexpy import helpers
data: function ( d ) {
return { 'json_data': JSON.stringify( d ),
'user_id': user_id,
'user': "${data['username']}"
'user': username
};
}
}
@@ -363,7 +368,7 @@ from plexpy import helpers
"url": "get_sync",
"data": function(d) {
d.user_id = user_id;
d.user = "${data['username']}";
d.user = username;
}
}
sync_table = $('#sync_table').DataTable(sync_table_options);
@@ -380,7 +385,7 @@ from plexpy import helpers
$("#edit-user-tooltip").tooltip('hide');
$.ajax({
url: 'edit_user_dialog',
data: { user_id: user_id, user: '${data['username']}' },
data: { user_id: user_id, user: username },
cache: false,
async: true,
complete: function(xhr, status) {
@@ -425,6 +430,33 @@ from plexpy import helpers
});
}
});
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: username, limit: containerSize },
complete: function(xhr, status) {
$("#user-recently-watched").html(xhr.responseText);
}
});
}
recentlyWatched();
$(window).resize(function() {

View File

@@ -16,7 +16,7 @@
<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 class="alert alert-danger alert-edit" role="alert" id="row-edit-mode-alert"><i class="fa fa-exclamation-triangle"></i>&nbspSelect users to delete/purge. Data is deleted/purged upon exiting edit mode.</div>
</div>
</div>
<div class='table-card-back'>
@@ -46,16 +46,16 @@
<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>
<h4 class="modal-title" id="myModalLabel">Confirm Delete/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>
<ul id="users-to-purge" 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>
<button type="button" class="btn btn-danger btn-ok" data-dismiss="modal" id="confirm-delete">Confirm</button>
</div>
</div>
</div>
@@ -74,8 +74,8 @@
<script>
$(document).ready(function () {
users_list_table_options.ajax = {
"url": "get_user_list",
type: "post",
url: 'get_user_list',
type: 'POST',
data: function ( d ) {
return { 'json_data': JSON.stringify( d ) };
}
@@ -88,18 +88,46 @@
$('#row-edit-mode').on('click', function () {
$('#row-edit-mode-alert').fadeIn(200);
$('#users-to-delete').html('');
$('#users-to-purge').html('');
if ($(this).hasClass('active')) {
if (users_to_purge.length > 0) {
if (users_to_delete.length > 0 || 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>');
users_to_purge = $.grep(users_to_purge, function (value) {
return $.inArray(value, users_to_delete) < 0;
});
if (users_to_delete.length > 0) {
$('#users-to-delete').prepend('<p>Are you REALLY sure you want to delete the following users:</p>')
for (var i = 0; i < users_to_delete.length; i++) {
$('#users-to-delete').append('<li>' + $('div[data-id=' + users_to_delete[i] + '] > input').val() + '</li>');
}
}
if (users_to_purge.length > 0) {
$('#users-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following users:</p>')
for (var i = 0; i < users_to_purge.length; i++) {
$('#users-to-purge').append('<li>' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '</li>');
}
}
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-purge', function () {
$('#confirm-modal').one('click', '#confirm-delete', function () {
for (var i = 0; i < users_to_delete.length; i++) {
$.ajax({
url: 'delete_user',
data: { user_id: users_to_delete[i] },
cache: false,
async: true,
success: function (data) {
var msg = "User deleted";
showMsg(msg, false, true, 2000);
}
});
}
for (var i = 0; i < users_to_purge.length; i++) {
$.ajax({
url: 'delete_all_user_history',
@@ -129,6 +157,7 @@
});
} else {
users_to_delete = [];
users_to_purge = [];
$('.edit-control').each(function () {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
@@ -147,12 +176,12 @@
url: 'refresh_users_list',
cache: false,
async: true,
success : function(data) {
success: function(data) {
showMsg('<i class="fa fa-check"></i>&nbspUser list refresh started...',false,true,2000,false)
},
error: function(jqXHR, textStatus, errorThrown) {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspUnable to refresh user list.',false,true,2000,true)
},
}
});
});
</script>

View File

@@ -1,4 +1,4 @@
<%
<%
import plexpy
from plexpy import common
%>
@@ -104,9 +104,15 @@ from plexpy import common
<div class="wizard-card" data-cardname="card4">
<h3>Monitoring</h3>
<p class="help-block">Keep records of all movie, TV show, or music items played from your Plex Media Server.</p>
<div class="wizard-input-section">
<input type="checkbox" id="video_logging_enable" name="video_logging_enable" value="1" ${config['video_logging_enable']}> Log Movies and TV
<p class="help-block">Keep records of all video items played from your Plex Media Server.</p>
<input type="checkbox" id="movie_logging_enable" name="movie_logging_enable" value="1" ${config['movie_logging_enable']}> Enable Movie Logging
</div>
<div class="wizard-input-section">
<input type="checkbox" id="tv_logging_enable" name="tv_logging_enable" value="1" ${config['tv_logging_enable']}> Enable TV Show Logging
</div>
<div class="wizard-input-section">
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1" ${config['music_logging_enable']}> Enable Music Logging
</div>
<div class="wizard-input-section">
<label for="logging_ignore_interval">Ignore Interval</label>
@@ -118,19 +124,16 @@ from plexpy import common
</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>
<!-- Music logging is still very experimental -- leave this for now.
<div class="wizard-input-section">
<input type="checkbox" id="music_logging_enable" name="music_logging_enable" value="1"> Log Music
<p class="help-block">Keep records of all audio items played from your Plex Media Server. VERY experimental.</p>
</div>
-->
</div>
<div class="wizard-card" data-cardname="card5" data-validate="validateNotifications">
<h3>Notifications</h3>
<p class="help-block">PlexPy supports a wide variety of notification options. To set up a notification agent conifgure this in <strong>Settings -> Notification Agents</strong>
after you have completed this setup wizard.</p><br/>
<div class="wizard-input-section">
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie and TV playback
<input type="checkbox" name="movie_notify_enable" id="movie_notify_enable" value="1" ${config['movie_notify_enable']}> Enable notifications on Movie playback
</div>
<div class="wizard-input-section">
<input type="checkbox" name="tv_notify_enable" id="tv_notify_enable" value="1" ${config['tv_notify_enable']}> Enable notifications on TV Show playback
</div>
<div class="wizard-input-section">
<input type="checkbox" name="music_notify_enable" id="music_notify_enable" value="1" ${config['music_notify_enable']}> Enable notifications on Music playback
@@ -262,6 +265,7 @@ from plexpy import common
if (ci != "undefined") {
// To allow next step in the guide.
// servers with clientIdentifier is verified
$("#pms_identifier").val(ci);
$("#pms_valid").val("valid");
$("#pms-verify-status").html('<i class="fa fa-check"></i> Server found!').show();
} else {

View File

@@ -31,7 +31,7 @@ except ImportError:
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger
from plexpy import versioncheck, logger, activity_pinger, plextv
from plexpy import versioncheck, logger, activity_pinger, plextv, pmsconnect
import plexpy.config
PROG_DIR = None
@@ -169,6 +169,7 @@ def initialize(config_file):
# Get the real PMS urls for SSL and remote access
if CONFIG.PMS_TOKEN and CONFIG.PMS_IP and CONFIG.PMS_PORT:
plextv.get_real_pms_url()
pmsconnect.get_server_friendly_name()
# Refresh the users list on startup
if CONFIG.PMS_TOKEN and CONFIG.REFRESH_USERS_ON_STARTUP:
@@ -280,7 +281,24 @@ def initialize_scheduler():
seconds = 0
if CONFIG.PMS_IP and CONFIG.PMS_TOKEN:
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs', hours=12, minutes=0, seconds=0)
schedule_job(plextv.get_real_pms_url, 'Refresh Plex Server URLs',
hours=12, minutes=0, seconds=0)
schedule_job(pmsconnect.get_server_friendly_name, 'Refresh Plex Server Name',
hours=12, minutes=0, seconds=0)
if CONFIG.NOTIFY_RECENTLY_ADDED:
schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
hours=0, minutes=0, seconds=seconds)
else:
schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
hours=0, minutes=0, seconds=0)
if CONFIG.MONITOR_REMOTE_ACCESS:
schedule_job(activity_pinger.check_server_response, 'Check for server response',
hours=0, minutes=0, seconds=seconds)
else:
schedule_job(activity_pinger.check_server_response, 'Check for server response',
hours=0, minutes=0, seconds=0)
# If we're not using websockets then fall back to polling
if not CONFIG.MONITORING_USE_WEBSOCKET or POLLING_FAILOVER:
@@ -398,9 +416,9 @@ def dbcheck():
c_db.execute(
'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, '
'friendly_name TEXT, thumb TEXT, email TEXT, custom_avatar_url TEXT, is_home_user INTEGER DEFAULT NULL, '
'is_allow_sync INTEGER DEFAULT NULL, is_restricted INTEGER DEFAULT NULL, do_notify INTEGER DEFAULT 1, '
'keep_history INTEGER DEFAULT 1, custom_avatar_url TEXT)'
'keep_history INTEGER DEFAULT 1, deleted_user INTEGER DEFAULT 0)'
)
# Upgrade sessions table from earlier versions
@@ -552,7 +570,7 @@ def dbcheck():
'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, '
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER)'
'on_pause INTEGER, on_resume INTEGER, on_buffer INTEGER, on_created INTEGER)'
)
# Upgrade users table from earlier versions
@@ -588,6 +606,15 @@ def dbcheck():
'ALTER TABLE notify_log ADD COLUMN on_buffer INTEGER'
)
# Upgrade notify_log table from earlier versions
try:
c_db.execute('SELECT on_created 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_created INTEGER'
)
# Upgrade sessions table from earlier versions
try:
c_db.execute('SELECT buffer_count from sessions')
@@ -647,6 +674,15 @@ def dbcheck():
'WHERE t1.id = session_history.id) '
)
# Upgrade users table from earlier versions
try:
c_db.execute('SELECT deleted_user from users')
except sqlite3.OperationalError:
logger.debug(u"Altering database. Updating database table users.")
c_db.execute(
'ALTER TABLE users ADD COLUMN deleted_user INTEGER DEFAULT 0'
)
conn_db.commit()
c_db.close()

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
@@ -212,4 +212,54 @@ class ActivityHandler(object):
else:
# We don't have this session in our table yet, start a new one.
if this_state != 'buffering':
self.on_start()
self.on_start()
class TimelineHandler(object):
def __init__(self, timeline):
self.timeline = timeline
#logger.debug(timeline)
def is_item(self):
if 'itemID' in self.timeline:
return True
return False
def get_rating_key(self):
if self.is_item():
return int(self.timeline['itemID'])
return None
def get_metadata(self):
pms_connect = pmsconnect.PmsConnect()
metadata_list = pms_connect.get_metadata_details(self.get_rating_key())
if metadata_list:
return metadata_list['metadata']
return None
def on_created(self):
if self.is_item():
logger.debug(u"PlexPy TimelineHandler :: Library item %s has been added to Plex." % str(self.get_rating_key()))
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(timeline_data=self.get_metadata(), notify_action='created')).start()
# This function receives events from our websocket connection
def process(self):
if self.is_item():
this_state = self.timeline['state']
this_type = self.timeline['type']
this_metadataState = self.timeline.get('metadataState', None)
this_mediaState = self.timeline.get('mediaState', None)
# state: 5: done processing metadata
# type: 1: movie, 2: tv show, 4: episode, 8: artist, 10: track
types = [1, 2, 4, 8, 10]
if this_state == 5 and this_type in types and this_metadataState == None and this_mediaState == None:
self.on_created()

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,13 +13,15 @@
# 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, pmsconnect, notification_handler, database, helpers, activity_processor
from plexpy import logger, pmsconnect, plextv, notification_handler, database, helpers, activity_processor
import threading
import plexpy
import time
monitor_lock = threading.Lock()
ext_ping_count = 0
int_ping_count = 0
def check_active_sessions(ws_request=False):
@@ -31,7 +33,16 @@ def check_active_sessions(ws_request=False):
monitor_process = activity_processor.ActivityProcessor()
# logger.debug(u"PlexPy Monitor :: Checking for active streams.")
global int_ping_count
if session_list:
if int_ping_count >= 3:
logger.info(u"PlexPy Monitor :: The Plex Media Server is back up.")
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(notify_action='intup')).start()
int_ping_count = 0
media_container = session_list['sessions']
# Check our temp table for what we must do with the new streams
@@ -121,7 +132,8 @@ def check_active_sessions(ws_request=False):
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]))
% (buffer_values[0]['buffer_count'],
buffer_values[0]['buffer_last_triggered']))
# 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
@@ -162,3 +174,112 @@ def check_active_sessions(ws_request=False):
monitor_process.write_session(session)
else:
logger.debug(u"PlexPy Monitor :: Unable to read session list.")
int_ping_count += 1
logger.warn(u"PlexPy Monitor :: Unable to get an internal response from the server, ping attempt %s." \
% str(int_ping_count))
if int_ping_count == 3:
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(notify_action='intdown')).start()
def check_recently_added():
with monitor_lock:
# add delay to allow for metadata processing
delay = plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY
time_threshold = int(time.time()) - delay
time_interval = plexpy.CONFIG.MONITORING_INTERVAL
pms_connect = pmsconnect.PmsConnect()
recently_added_list = pms_connect.get_recently_added_details(count='10')
if recently_added_list:
recently_added = recently_added_list['recently_added']
for item in recently_added:
metadata = []
if 0 < time_threshold - int(item['added_at']) <= time_interval:
if item['media_type'] == 'movie':
metadata_list = pms_connect.get_metadata_details(item['rating_key'])
if metadata_list:
metadata = [metadata_list['metadata']]
else:
logger.error(u"PlexPy Monitor :: Unable to retrieve metadata for rating_key %s" \
% str(item['rating_key']))
else:
metadata_list = pms_connect.get_metadata_children_details(item['rating_key'])
if metadata_list:
metadata = metadata_list['metadata']
else:
logger.error(u"PlexPy Monitor :: Unable to retrieve children metadata for rating_key %s" \
% str(item['rating_key']))
if metadata:
if not plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
for item in metadata:
if 0 < time_threshold - int(item['added_at']) <= time_interval:
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(timeline_data=item, notify_action='created')).start()
else:
item = max(metadata, key=lambda x:x['added_at'])
if 0 < time_threshold - int(item['added_at']) <= time_interval:
if item['media_type'] == 'episode' or item['media_type'] == 'track':
metadata_list = pms_connect.get_metadata_details(item['grandparent_rating_key'])
if metadata_list:
item = metadata_list['metadata']
else:
logger.error(u"PlexPy Monitor :: Unable to retrieve grandparent metadata for grandparent_rating_key %s" \
% str(item['rating_key']))
logger.debug(u"PlexPy Monitor :: Library item %s has been added to Plex." % str(item['rating_key']))
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(timeline_data=item, notify_action='created')).start()
def check_server_response():
with monitor_lock:
pms_connect = pmsconnect.PmsConnect()
server_response = pms_connect.get_server_response()
global ext_ping_count
# Check for remote access
if server_response:
mapping_state = server_response['mapping_state']
mapping_error = server_response['mapping_error']
# Check if the port is mapped
if not mapping_state == 'mapped':
ext_ping_count += 1
logger.warn(u"PlexPy Monitor :: Plex remote access port not mapped, ping attempt %s." \
% str(ext_ping_count))
# Check if the port is open
elif mapping_error == 'unreachable':
ext_ping_count += 1
logger.warn(u"PlexPy Monitor :: Plex remote access port mapped, but mapping failed, ping attempt %s." \
% str(ext_ping_count))
# Reset external ping counter
else:
if ext_ping_count >= 3:
logger.info(u"PlexPy Monitor :: Plex remote access is back up.")
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(notify_action='extup')).start()
ext_ping_count = 0
if ext_ping_count == 3:
# Fire off notifications
threading.Thread(target=notification_handler.notify_timeline,
kwargs=dict(notify_action='extdown')).start()

View File

@@ -39,6 +39,7 @@ class ActivityProcessor(object):
'parent_title': session['parent_title'],
'grandparent_title': session['grandparent_title'],
'friendly_name': session['friendly_name'],
#'ip_address': session['ip_address'],
'player': session['player'],
'platform': session['platform'],
'parent_rating_key': session['parent_rating_key'],
@@ -66,6 +67,10 @@ class ActivityProcessor(object):
'transcode_height': session['transcode_height']
}
# Add ip_address back into values
if session['ip_address']:
values.update({'ip_address': session['ip_address']})
keys = {'session_key': session['session_key'],
'rating_key': session['rating_key']}
@@ -74,24 +79,23 @@ class ActivityProcessor(object):
if result == 'insert':
# Push any notifications - Push it on it's own thread so we don't hold up our db actions
if notify:
values.update({'ip_address': session['ip_address']})
threading.Thread(target=notification_handler.notify,
kwargs=dict(stream_data=values, notify_action='play')).start()
started = int(time.time())
# Try and grab IP address from logs
if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER:
ip_address = self.find_session_ip(rating_key=session['rating_key'],
machine_id=session['machine_id'])
else:
ip_address = None
timestamp = {'started': started,
'ip_address': ip_address}
# If it's our first write then time stamp it.
started = int(time.time())
timestamp = {'started': started}
self.db.upsert('sessions', timestamp, keys)
# Try and grab IP address from logs (fallback if not on PMS 0.9.14 and above)
if not session['ip_address']:
if plexpy.CONFIG.IP_LOGGING_ENABLE and plexpy.CONFIG.PMS_LOGS_FOLDER:
ip_address = self.find_session_ip(rating_key=session['rating_key'],
machine_id=session['machine_id'])
ip_address = {'ip_address': ip_address}
self.db.upsert('sessions', ip_address, keys)
def write_session_history(self, session=None, import_metadata=None, is_import=False, import_ignore_interval=0):
from plexpy import users
@@ -109,8 +113,11 @@ class ActivityProcessor(object):
else:
stopped = int(time.time())
if plexpy.CONFIG.VIDEO_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
(session['media_type'] == 'movie' or session['media_type'] == 'episode'):
if plexpy.CONFIG.MOVIE_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
session['media_type'] == 'movie':
logging_enabled = True
elif plexpy.CONFIG.TV_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
session['media_type'] == 'episode':
logging_enabled = True
elif plexpy.CONFIG.MUSIC_LOGGING_ENABLE and str(session['rating_key']).isdigit() and \
session['media_type'] == 'track':
@@ -173,18 +180,18 @@ class ActivityProcessor(object):
result = self.db.select(query=query, args=args)
new_session = {'id': result[0][0],
'rating_key': result[0][1],
'user_id': result[0][2],
'reference_id': result[0][3]}
new_session = {'id': result[0]['id'],
'rating_key': result[0]['rating_key'],
'user_id': result[0]['user_id'],
'reference_id': result[0]['reference_id']}
if len(result) == 1:
prev_session = None
else:
prev_session = {'id': result[1][0],
'rating_key': result[1][1],
'user_id': result[1][2],
'reference_id': result[1][3]}
prev_session = {'id': result[1]['id'],
'rating_key': result[1]['rating_key'],
'user_id': result[1]['user_id'],
'reference_id': result[1]['reference_id']}
query = 'UPDATE session_history SET reference_id = ? WHERE id = ? '
# If rating_key is the same in the previous session, then set the reference_id to the previous row, else set the reference_id to the new id
@@ -278,9 +285,16 @@ class ActivityProcessor(object):
if ipv4:
# The logged IP will always be the first match and we don't want localhost entries
if ipv4[0] != '127.0.0.1':
# check if IPv4 mapped IPv6 address (::ffff:xxx.xxx.xxx.xxx)
#if '::ffff:' + ipv4[0] in line:
# logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s "
# u"and machineIdentifier %s."
# % ('::ffff:' + ipv4[0], rating_key, machine_id))
# return '::ffff:' + ipv4[0]
#else:
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s "
u"and machineIdentifier %s."
% (ipv4[0], rating_key, machine_id))
u"and machineIdentifier %s."
% (ipv4[0], rating_key, machine_id))
return ipv4[0]
logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on first pass. "
@@ -301,8 +315,13 @@ class ActivityProcessor(object):
if ipv4:
# The logged IP will always be the first match and we don't want localhost entries
if ipv4[0] != '127.0.0.1':
#if '::ffff:' + ipv4[0] in line:
# logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." %
# ('::ffff:' + ipv4[0], rating_key))
# return '::ffff:' + ipv4[0]
#else:
logger.debug(u"PlexPy ActivityProcessor :: Matched IP address (%s) for stream ratingKey %s." %
(ipv4[0], rating_key))
(ipv4[0], rating_key))
return ipv4[0]
logger.debug(u"PlexPy ActivityProcessor :: Unable to find IP address on fallback search. Not logging IP address.")
@@ -381,7 +400,7 @@ class ActivityProcessor(object):
'WHERE session_key = ?',
[session_key])
if buffer_count:
return buffer_count
return buffer_count['buffer_count']
return 0
@@ -398,6 +417,6 @@ class ActivityProcessor(object):
'WHERE session_key = ?',
[session_key])
if last_time:
return last_time
return last_time['buffer_last_triggered']
return None

View File

@@ -394,8 +394,8 @@ class Api(object):
"grouping_global_history": bool(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
"grouping_user_history": bool(plexpy.CONFIG.GROUPING_USER_HISTORY),
"grouping_charts": bool(plexpy.CONFIG.GROUPING_CHARTS),
"tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": bool(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"tv_notify_enable": bool(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"music_notify_enable": bool(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": bool(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": bool(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
@@ -410,7 +410,8 @@ class Api(object):
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
"refresh_users_on_startup": bool(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
"ip_logging_enable": bool(plexpy.CONFIG.IP_LOGGING_ENABLE),
"video_logging_enable": bool(plexpy.CONFIG.VIDEO_LOGGING_ENABLE),
"movie_logging_enable": bool(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
"tv_logging_enable": bool(plexpy.CONFIG.TV_LOGGING_ENABLE),
"music_logging_enable": bool(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"pms_is_remote": bool(plexpy.CONFIG.PMS_IS_REMOTE),

View File

@@ -41,4 +41,9 @@ notify_strings[NOTIFY_STOPPED] = "Playback stopped"
DEFAULT_USER_THUMB = "interfaces/default/images/gravatar-default-80x80.png"
DEFAULT_POSTER_THUMB = "interfaces/default/images/poster.png"
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
DEFAULT_COVER_THUMB = "interfaces/default/images/cover.png"
PLATFORM_NAME_OVERRIDES = {'Konvergo': 'Plex Media Player',
'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}

View File

@@ -26,6 +26,7 @@ _CONFIG_DEFINITIONS = {
'PMS_IP': (str, 'PMS', '127.0.0.1'),
'PMS_IS_REMOTE': (int, 'PMS', 0),
'PMS_LOGS_FOLDER': (str, 'PMS', ''),
'PMS_NAME': (unicode, 'PMS', ''),
'PMS_PORT': (int, 'PMS', 32400),
'PMS_TOKEN': (str, 'PMS', ''),
'PMS_SSL': (int, 'General', 0),
@@ -44,6 +45,11 @@ _CONFIG_DEFINITIONS = {
'BOXCAR_ON_RESUME': (int, 'Boxcar', 0),
'BOXCAR_ON_BUFFER': (int, 'Boxcar', 0),
'BOXCAR_ON_WATCHED': (int, 'Boxcar', 0),
'BOXCAR_ON_CREATED': (int, 'Boxcar', 0),
'BOXCAR_ON_EXTDOWN': (int, 'Boxcar', 0),
'BOXCAR_ON_INTDOWN': (int, 'Boxcar', 0),
'BOXCAR_ON_EXTUP': (int, 'Boxcar', 0),
'BOXCAR_ON_INTUP': (int, 'Boxcar', 0),
'BUFFER_THRESHOLD': (int, 'Monitoring', 3),
'BUFFER_WAIT': (int, 'Monitoring', 900),
'CACHE_DIR': (str, 'General', ''),
@@ -57,6 +63,8 @@ _CONFIG_DEFINITIONS = {
'EMAIL_ENABLED': (int, 'Email', 0),
'EMAIL_FROM': (str, 'Email', ''),
'EMAIL_TO': (str, 'Email', ''),
'EMAIL_CC': (str, 'Email', ''),
'EMAIL_BCC': (str, 'Email', ''),
'EMAIL_SMTP_SERVER': (str, 'Email', ''),
'EMAIL_SMTP_USER': (str, 'Email', ''),
'EMAIL_SMTP_PASSWORD': (str, 'Email', ''),
@@ -68,12 +76,20 @@ _CONFIG_DEFINITIONS = {
'EMAIL_ON_RESUME': (int, 'Email', 0),
'EMAIL_ON_BUFFER': (int, 'Email', 0),
'EMAIL_ON_WATCHED': (int, 'Email', 0),
'EMAIL_ON_CREATED': (int, 'Email', 0),
'EMAIL_ON_EXTDOWN': (int, 'Email', 0),
'EMAIL_ON_INTDOWN': (int, 'Email', 0),
'EMAIL_ON_EXTUP': (int, 'Email', 0),
'EMAIL_ON_INTUP': (int, 'Email', 0),
'ENABLE_HTTPS': (int, 'General', 0),
'FIRST_RUN_COMPLETE': (int, 'General', 0),
'FREEZE_DB': (int, 'General', 0),
'GIT_BRANCH': (str, 'General', 'master'),
'GIT_PATH': (str, 'General', ''),
'GIT_USER': (str, 'General', 'drzoidberg33'),
'GRAPH_TYPE': (str, 'General', 'plays'),
'GRAPH_DAYS': (int, 'General', 30),
'GRAPH_TAB': (str, 'General', 'tabs-1'),
'GROUP_HISTORY_TABLES': (int, 'General', 0),
'GROWL_ENABLED': (int, 'Growl', 0),
'GROWL_HOST': (str, 'Growl', ''),
@@ -84,11 +100,17 @@ _CONFIG_DEFINITIONS = {
'GROWL_ON_RESUME': (int, 'Growl', 0),
'GROWL_ON_BUFFER': (int, 'Growl', 0),
'GROWL_ON_WATCHED': (int, 'Growl', 0),
'GROWL_ON_CREATED': (int, 'Growl', 0),
'GROWL_ON_EXTDOWN': (int, 'Growl', 0),
'GROWL_ON_INTDOWN': (int, 'Growl', 0),
'GROWL_ON_EXTUP': (int, 'Growl', 0),
'GROWL_ON_INTUP': (int, 'Growl', 0),
'HOME_LIBRARY_CARDS': (str, 'General', 'library_statistics_first'),
'HOME_STATS_LENGTH': (int, 'General', 30),
'HOME_STATS_TYPE': (int, 'General', 0),
'HOME_STATS_COUNT': (int, 'General', 5),
'HOME_STATS_CARDS': (str, 'General', 'watch_statistics, top_tv, popular_tv, top_movies, popular_movies, top_music, popular_music, top_users, top_platforms, last_watched'),
'HOME_STATS_CARDS': (str, 'General', 'watch_statistics, top_tv, popular_tv, top_movies, popular_movies, ' \
'top_music, popular_music, last_watched, top_users, top_platforms, most_concurrent'),
'HTTPS_CERT': (str, 'General', ''),
'HTTPS_KEY': (str, 'General', ''),
'HTTP_HOST': (str, 'General', '0.0.0.0'),
@@ -99,19 +121,35 @@ _CONFIG_DEFINITIONS = {
'HTTP_USERNAME': (str, 'General', ''),
'INTERFACE': (str, 'General', 'default'),
'IP_LOGGING_ENABLE': (int, 'General', 0),
'IFTTT_KEY': (str, 'IFTTT', ''),
'IFTTT_EVENT': (str, 'IFTTT', 'plexpy'),
'IFTTT_ENABLED': (int, 'IFTTT', 0),
'IFTTT_ON_PLAY': (int, 'IFTTT', 0),
'IFTTT_ON_STOP': (int, 'IFTTT', 0),
'IFTTT_ON_PAUSE': (int, 'IFTTT', 0),
'IFTTT_ON_RESUME': (int, 'IFTTT', 0),
'IFTTT_ON_BUFFER': (int, 'IFTTT', 0),
'IFTTT_ON_WATCHED': (int, 'IFTTT', 0),
'IFTTT_ON_CREATED': (int, 'IFTTT', 0),
'IFTTT_ON_EXTDOWN': (int, 'IFTTT', 0),
'IFTTT_ON_INTDOWN': (int, 'IFTTT', 0),
'IFTTT_ON_EXTUP': (int, 'IFTTT', 0),
'IFTTT_ON_INTUP': (int, 'IFTTT', 0),
'JOURNAL_MODE': (str, 'Advanced', 'wal'),
'LAUNCH_BROWSER': (int, 'General', 1),
'LOG_DIR': (str, 'General', ''),
'LOGGING_IGNORE_INTERVAL': (int, 'Monitoring', 120),
'MOVIE_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'MOVIE_NOTIFY_ON_START': (int, 'Monitoring', 1),
'MOVIE_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'MOVIE_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 1),
'MUSIC_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'MUSIC_NOTIFY_ON_START': (int, 'Monitoring', 1),
'MUSIC_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
'MUSIC_NOTIFY_ON_PAUSE': (int, 'Monitoring', 0),
'MUSIC_LOGGING_ENABLE': (int, 'Monitoring', 0),
'MONITOR_REMOTE_ACCESS': (int, 'Monitoring', 0),
'MONITORING_INTERVAL': (int, 'Monitoring', 60),
'MONITORING_USE_WEBSOCKET': (int, 'Monitoring', 0),
'NMA_APIKEY': (str, 'NMA', ''),
@@ -123,20 +161,38 @@ _CONFIG_DEFINITIONS = {
'NMA_ON_RESUME': (int, 'NMA', 0),
'NMA_ON_BUFFER': (int, 'NMA', 0),
'NMA_ON_WATCHED': (int, 'NMA', 0),
'NMA_ON_CREATED': (int, 'NMA', 0),
'NMA_ON_EXTDOWN': (int, 'NMA', 0),
'NMA_ON_INTDOWN': (int, 'NMA', 0),
'NMA_ON_EXTUP': (int, 'NMA', 0),
'NMA_ON_INTUP': (int, 'NMA', 0),
'NOTIFY_CONSECUTIVE': (int, 'Monitoring', 1),
'NOTIFY_RECENTLY_ADDED': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_GRANDPARENT': (int, 'Monitoring', 0),
'NOTIFY_RECENTLY_ADDED_DELAY': (int, 'Monitoring', 60),
'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}.'),
'NOTIFY_ON_START_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_START_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) started playing {title}.'),
'NOTIFY_ON_STOP_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_STOP_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has stopped {title}.'),
'NOTIFY_ON_PAUSE_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_PAUSE_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has paused {title}.'),
'NOTIFY_ON_RESUME_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_RESUME_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has resumed {title}.'),
'NOTIFY_ON_BUFFER_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_BUFFER_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) is buffering {title}.'),
'NOTIFY_ON_WATCHED_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_WATCHED_BODY_TEXT': (unicode, 'Monitoring', '{user} ({player}) has watched {title}.'),
'NOTIFY_ON_CREATED_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_CREATED_BODY_TEXT': (unicode, 'Monitoring', '{title} was recently added to Plex.'),
'NOTIFY_ON_EXTDOWN_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_EXTDOWN_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is down.'),
'NOTIFY_ON_INTDOWN_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_INTDOWN_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is down.'),
'NOTIFY_ON_EXTUP_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_EXTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server remote access is back up.'),
'NOTIFY_ON_INTUP_SUBJECT_TEXT': (unicode, 'Monitoring', 'PlexPy ({server_name})'),
'NOTIFY_ON_INTUP_BODY_TEXT': (unicode, 'Monitoring', 'The Plex Media Server is back up.'),
'OSX_NOTIFY_APP': (str, 'OSX_Notify', '/Applications/PlexPy'),
'OSX_NOTIFY_ENABLED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_PLAY': (int, 'OSX_Notify', 0),
@@ -145,6 +201,11 @@ _CONFIG_DEFINITIONS = {
'OSX_NOTIFY_ON_RESUME': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_BUFFER': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_WATCHED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_CREATED': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_EXTDOWN': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_INTDOWN': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_EXTUP': (int, 'OSX_Notify', 0),
'OSX_NOTIFY_ON_INTUP': (int, 'OSX_Notify', 0),
'PLEX_CLIENT_HOST': (str, 'Plex', ''),
'PLEX_ENABLED': (int, 'Plex', 0),
'PLEX_PASSWORD': (str, 'Plex', ''),
@@ -155,6 +216,11 @@ _CONFIG_DEFINITIONS = {
'PLEX_ON_RESUME': (int, 'Plex', 0),
'PLEX_ON_BUFFER': (int, 'Plex', 0),
'PLEX_ON_WATCHED': (int, 'Plex', 0),
'PLEX_ON_CREATED': (int, 'Plex', 0),
'PLEX_ON_EXTDOWN': (int, 'Plex', 0),
'PLEX_ON_INTDOWN': (int, 'Plex', 0),
'PLEX_ON_EXTUP': (int, 'Plex', 0),
'PLEX_ON_INTUP': (int, 'Plex', 0),
'PROWL_ENABLED': (int, 'Prowl', 0),
'PROWL_KEYS': (str, 'Prowl', ''),
'PROWL_PRIORITY': (int, 'Prowl', 0),
@@ -164,6 +230,11 @@ _CONFIG_DEFINITIONS = {
'PROWL_ON_RESUME': (int, 'Prowl', 0),
'PROWL_ON_BUFFER': (int, 'Prowl', 0),
'PROWL_ON_WATCHED': (int, 'Prowl', 0),
'PROWL_ON_CREATED': (int, 'Prowl', 0),
'PROWL_ON_EXTDOWN': (int, 'Prowl', 0),
'PROWL_ON_INTDOWN': (int, 'Prowl', 0),
'PROWL_ON_EXTUP': (int, 'Prowl', 0),
'PROWL_ON_INTUP': (int, 'Prowl', 0),
'PUSHALOT_APIKEY': (str, 'Pushalot', ''),
'PUSHALOT_ENABLED': (int, 'Pushalot', 0),
'PUSHALOT_ON_PLAY': (int, 'Pushalot', 0),
@@ -172,6 +243,11 @@ _CONFIG_DEFINITIONS = {
'PUSHALOT_ON_RESUME': (int, 'Pushalot', 0),
'PUSHALOT_ON_BUFFER': (int, 'Pushalot', 0),
'PUSHALOT_ON_WATCHED': (int, 'Pushalot', 0),
'PUSHALOT_ON_CREATED': (int, 'Pushalot', 0),
'PUSHALOT_ON_EXTDOWN': (int, 'Pushalot', 0),
'PUSHALOT_ON_INTDOWN': (int, 'Pushalot', 0),
'PUSHALOT_ON_EXTUP': (int, 'Pushalot', 0),
'PUSHALOT_ON_INTUP': (int, 'Pushalot', 0),
'PUSHBULLET_APIKEY': (str, 'PushBullet', ''),
'PUSHBULLET_DEVICEID': (str, 'PushBullet', ''),
'PUSHBULLET_CHANNEL_TAG': (str, 'PushBullet', ''),
@@ -182,6 +258,11 @@ _CONFIG_DEFINITIONS = {
'PUSHBULLET_ON_RESUME': (int, 'PushBullet', 0),
'PUSHBULLET_ON_BUFFER': (int, 'PushBullet', 0),
'PUSHBULLET_ON_WATCHED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_CREATED': (int, 'PushBullet', 0),
'PUSHBULLET_ON_EXTDOWN': (int, 'PushBullet', 0),
'PUSHBULLET_ON_INTDOWN': (int, 'PushBullet', 0),
'PUSHBULLET_ON_EXTUP': (int, 'PushBullet', 0),
'PUSHBULLET_ON_INTUP': (int, 'PushBullet', 0),
'PUSHOVER_APITOKEN': (str, 'Pushover', ''),
'PUSHOVER_ENABLED': (int, 'Pushover', 0),
'PUSHOVER_KEYS': (str, 'Pushover', ''),
@@ -193,8 +274,28 @@ _CONFIG_DEFINITIONS = {
'PUSHOVER_ON_RESUME': (int, 'Pushover', 0),
'PUSHOVER_ON_BUFFER': (int, 'Pushover', 0),
'PUSHOVER_ON_WATCHED': (int, 'Pushover', 0),
'PUSHOVER_ON_CREATED': (int, 'Pushover', 0),
'PUSHOVER_ON_EXTDOWN': (int, 'Pushover', 0),
'PUSHOVER_ON_INTDOWN': (int, 'Pushover', 0),
'PUSHOVER_ON_EXTUP': (int, 'Pushover', 0),
'PUSHOVER_ON_INTUP': (int, 'Pushover', 0),
'REFRESH_USERS_INTERVAL': (int, 'Monitoring', 12),
'REFRESH_USERS_ON_STARTUP': (int, 'Monitoring', 1),
'TELEGRAM_BOT_TOKEN': (str, 'Telegram', ''),
'TELEGRAM_ENABLED': (int, 'Telegram', 0),
'TELEGRAM_CHAT_ID': (str, 'Telegram', ''),
'TELEGRAM_ON_PLAY': (int, 'Telegram', 0),
'TELEGRAM_ON_STOP': (int, 'Telegram', 0),
'TELEGRAM_ON_PAUSE': (int, 'Telegram', 0),
'TELEGRAM_ON_RESUME': (int, 'Telegram', 0),
'TELEGRAM_ON_BUFFER': (int, 'Telegram', 0),
'TELEGRAM_ON_WATCHED': (int, 'Telegram', 0),
'TELEGRAM_ON_CREATED': (int, 'Telegram', 0),
'TELEGRAM_ON_EXTDOWN': (int, 'Telegram', 0),
'TELEGRAM_ON_INTDOWN': (int, 'Telegram', 0),
'TELEGRAM_ON_EXTUP': (int, 'Telegram', 0),
'TELEGRAM_ON_INTUP': (int, 'Telegram', 0),
'TV_LOGGING_ENABLE': (int, 'Monitoring', 1),
'TV_NOTIFY_ENABLE': (int, 'Monitoring', 0),
'TV_NOTIFY_ON_START': (int, 'Monitoring', 1),
'TV_NOTIFY_ON_STOP': (int, 'Monitoring', 0),
@@ -209,6 +310,11 @@ _CONFIG_DEFINITIONS = {
'TWITTER_ON_RESUME': (int, 'Twitter', 0),
'TWITTER_ON_BUFFER': (int, 'Twitter', 0),
'TWITTER_ON_WATCHED': (int, 'Twitter', 0),
'TWITTER_ON_CREATED': (int, 'Twitter', 0),
'TWITTER_ON_EXTDOWN': (int, 'Twitter', 0),
'TWITTER_ON_INTDOWN': (int, 'Twitter', 0),
'TWITTER_ON_EXTUP': (int, 'Twitter', 0),
'TWITTER_ON_INTUP': (int, 'Twitter', 0),
'UPDATE_DB_INTERVAL': (int, 'General', 24),
'VERIFY_SSL_CERT': (bool_int, 'Advanced', 1),
'VIDEO_LOGGING_ENABLE': (int, 'Monitoring', 1),
@@ -221,7 +327,12 @@ _CONFIG_DEFINITIONS = {
'XBMC_ON_PAUSE': (int, 'XBMC', 0),
'XBMC_ON_RESUME': (int, 'XBMC', 0),
'XBMC_ON_BUFFER': (int, 'XBMC', 0),
'XBMC_ON_WATCHED': (int, 'XBMC', 0)
'XBMC_ON_WATCHED': (int, 'XBMC', 0),
'XBMC_ON_CREATED': (int, 'XBMC', 0),
'XBMC_ON_EXTDOWN': (int, 'XBMC', 0),
'XBMC_ON_INTDOWN': (int, 'XBMC', 0),
'XBMC_ON_EXTUP': (int, 'XBMC', 0),
'XBMC_ON_INTUP': (int, 'XBMC', 0)
}
# pylint:disable=R0902
# it might be nice to refactor for fewer instance variables
@@ -234,6 +345,7 @@ class Config(object):
self._config = ConfigObj(self._config_file, encoding='utf-8')
for key in _CONFIG_DEFINITIONS.keys():
self.check_setting(key)
self._upgrade()
def _define(self, name):
key = name.upper()
@@ -322,4 +434,18 @@ class Config(object):
"""
for name, value in kwargs.items():
key, definition_type, section, ini_key, default = self._define(name)
self._config[section][ini_key] = definition_type(value)
self._config[section][ini_key] = definition_type(value)
def _upgrade(self):
"""
Upgrades config file from previous verisions and bumps up config version
"""
if self.CONFIG_VERSION == '0':
# Separate out movie and tv notifications
if self.MOVIE_NOTIFY_ENABLE == 1:
self.TV_NOTIFY_ENABLE = 1
# Separate out movie and tv logging
if self.VIDEO_LOGGING_ENABLE == 0:
self.MOVIE_LOGGING_ENABLE = 0
self.TV_LOGGING_ENABLE = 0
self.CONFIG_VERSION = '1'

View File

@@ -46,6 +46,13 @@ def get_cache_size():
return 0
return int(plexpy.CONFIG.CACHE_SIZEMB)
def dict_factory(cursor, row):
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return d
class MonitorDatabase(object):
@@ -58,7 +65,7 @@ class MonitorDatabase(object):
self.connection.execute("PRAGMA journal_mode = %s" % plexpy.CONFIG.JOURNAL_MODE)
# 64mb of cache memory, probably need to make it user configurable
self.connection.execute("PRAGMA cache_size=-%s" % (get_cache_size() * 1024))
self.connection.row_factory = sqlite3.Row
self.connection.row_factory = dict_factory
def action(self, query, args=None, return_last_id=False):
if query is None:
@@ -104,7 +111,7 @@ class MonitorDatabase(object):
def select_single(self, query, args=None):
sql_results = self.action(query, args).fetchone()[0]
sql_results = self.action(query, args).fetchone()
if sql_results is None or sql_results == "":
return ""

File diff suppressed because it is too large Load Diff

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
@@ -70,6 +70,39 @@ class DataTables(object):
else:
grouping = False
# Build join parameters
if join_types:
counter = 0
for join_type in join_types:
if join_type.upper() == 'LEFT OUTER JOIN':
join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN':
join_item = 'JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
else:
join_item = ''
counter += 1
join += join_item
# Build custom where parameters
if custom_where:
for w in custom_where:
c_where += w[0] + ' = ? AND '
# The order of our args changes if we are grouping
#if grouping:
# args.insert(0, w[1])
#else:
# args.append(w[1])
# My testing shows that order of args doesn't change
args.append(w[1])
if c_where:
c_where = 'WHERE ' + c_where.rstrip(' AND ')
# Build ordering
for o in parameters['order']:
sort_order = ' COLLATE NOCASE'
@@ -119,36 +152,6 @@ class DataTables(object):
if where:
where = 'WHERE ' + where.rstrip(' OR ')
# Build join parameters
if join_types:
counter = 0
for join_type in join_types:
if join_type.upper() == 'LEFT OUTER JOIN':
join_item = 'LEFT OUTER JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
elif join_type.upper() == 'JOIN' or join_type.upper() == 'INNER JOIN':
join_item = 'JOIN %s ON %s = %s ' % \
(join_tables[counter], join_evals[counter][0], join_evals[counter][1])
else:
join_item = ''
counter += 1
join += join_item
# Build custom where parameters
if custom_where:
for w in custom_where:
c_where += w[0] + ' = ? AND '
# The order of our args changes if we are grouping
if grouping:
args.insert(0, w[1])
else:
args.append(w[1])
if c_where:
c_where = 'WHERE ' + c_where.rstrip(' AND ')
# Build our queries
if grouping:
if c_where == '':
@@ -175,12 +178,18 @@ class DataTables(object):
filtered = self.ssp_db.select(query, args=args)
# Build grand totals
totalcount = self.ssp_db.select('SELECT COUNT(id) from %s' % table_name)[0][0]
totalcount = self.ssp_db.select('SELECT COUNT(id) as total_count from %s' % table_name)[0]['total_count']
# Get draw counter
draw_counter = int(parameters['draw'])
# Paginate results
result = filtered[parameters['start']:(parameters['start'] + parameters['length'])]
# Sanitize on the way out
result = [{k: helpers.sanitize(v) if isinstance(v, basestring) else v for k, v in row.iteritems()}
for row in result]
output = {'result': result,
'draw': draw_counter,
'filteredCount': len(filtered),

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, database, helpers
from plexpy import logger, database, helpers, common
import datetime
@@ -44,11 +44,11 @@ class Graphs(object):
else:
query = 'SELECT date(started, "unixepoch", "localtime") as date_played, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= datetime("now", "-%s days", "localtime") ' \
'GROUP BY date_played ' \
@@ -76,10 +76,10 @@ class Graphs(object):
series_2_value = 0
series_3_value = 0
for item in result:
if date_string == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
if date_string == item['date_played']:
series_1_value = item['tv_count']
series_2_value = item['movie_count']
series_3_value = item['music_count']
break
else:
series_1_value = 0
@@ -138,11 +138,11 @@ class Graphs(object):
'when 5 then "Friday" ' \
'else "Saturday" end as dayofweek, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
@@ -165,10 +165,10 @@ class Graphs(object):
series_2_value = 0
series_3_value = 0
for item in result:
if day_item == item[1]:
series_1_value = item[2]
series_2_value = item[3]
series_3_value = item[4]
if day_item == item['dayofweek']:
series_1_value = item['tv_count']
series_2_value = item['movie_count']
series_3_value = item['music_count']
break
else:
series_1_value = 0
@@ -211,11 +211,11 @@ class Graphs(object):
else:
query = 'select strftime("%H", datetime(started, "unixepoch", "localtime")) as hourofday, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(stopped, "unixepoch", "localtime") >= ' \
'datetime("now", "-' + time_range + ' days", "localtime") ' \
@@ -240,10 +240,10 @@ class Graphs(object):
series_2_value = 0
series_3_value = 0
for item in result:
if hour_item == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
if hour_item == item['hourofday']:
series_1_value = item['tv_count']
series_2_value = item['movie_count']
series_3_value = item['music_count']
break
else:
series_1_value = 0
@@ -283,11 +283,11 @@ class Graphs(object):
else:
query = 'SELECT strftime("%Y-%m", datetime(started, "unixepoch", "localtime")) as datestring, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_count ' \
'FROM session_history ' \
'WHERE datetime(started, "unixepoch", "localtime") >= datetime("now", "-12 months", "localtime") ' \
'GROUP BY strftime("%Y-%m", datetime(started, "unixepoch", "localtime")) ' \
@@ -316,10 +316,10 @@ class Graphs(object):
series_2_value = 0
series_3_value = 0
for item in result:
if date_string == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
if date_string == item['datestring']:
series_1_value = item['tv_count']
series_2_value = item['movie_count']
series_3_value = item['music_count']
break
else:
series_1_value = 0
@@ -364,11 +364,11 @@ class Graphs(object):
else:
query = 'SELECT platform, ' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_count, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
@@ -386,17 +386,10 @@ class Graphs(object):
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
# Rename Mystery platform names
platform_names = [('Mystery 3', 'Playstation 3'),
('Mystery 4', 'Playstation 4'),
('Mystery 5', 'Xbox 360')]
for old_name, new_name in platform_names:
categories = [item.replace(old_name, new_name) for item in categories]
categories.append(common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']))
series_1.append(item['tv_count'])
series_2.append(item['movie_count'])
series_3.append(item['music_count'])
series_1_output = {'name': 'TV',
'data': series_1}
@@ -417,7 +410,7 @@ class Graphs(object):
if y_axis == 'plays':
query = 'SELECT ' \
'(case when users.friendly_name is null then session_history.user else ' \
'(case when users.friendly_name is null then users.username else ' \
'users.friendly_name end) as friendly_name,' \
'SUM(case when media_type = "episode" then 1 else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" then 1 else 0 end) as movie_count, ' \
@@ -434,14 +427,14 @@ class Graphs(object):
result = monitor_db.select(query)
else:
query = 'SELECT ' \
'(case when users.friendly_name is null then session_history.user else ' \
'(case when users.friendly_name is null then users.username else ' \
'users.friendly_name end) as friendly_name,' \
'SUM(case when media_type = "episode" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tv_count, ' \
'SUM(case when media_type = "movie" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as movie_count, ' \
'SUM(case when media_type = "track" and stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as music_count, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
@@ -460,10 +453,10 @@ class Graphs(object):
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
categories.append(item['friendly_name'])
series_1.append(item['tv_count'])
series_2.append(item['movie_count'])
series_3.append(item['music_count'])
series_1_output = {'name': 'TV',
'data': series_1}
@@ -508,15 +501,15 @@ class Graphs(object):
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_count, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_count, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_count ' \
'FROM session_history ' \
'JOIN session_history_media_info ON session_history.id = session_history_media_info.id ' \
'WHERE datetime(session_history.stopped, "unixepoch", "localtime") >= ' \
@@ -547,10 +540,10 @@ class Graphs(object):
series_2_value = 0
series_3_value = 0
for item in result:
if date_string == item[0]:
series_1_value = item[1]
series_2_value = item[2]
series_3_value = item[3]
if date_string == item['date_played']:
series_1_value = item['dp_count']
series_2_value = item['ds_count']
series_3_value = item['tc_count']
break
else:
series_1_value = 0
@@ -605,15 +598,15 @@ class Graphs(object):
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_count, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_count, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_count, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
@@ -633,10 +626,10 @@ class Graphs(object):
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
categories.append(item['resolution'])
series_1.append(item['dp_count'])
series_2.append(item['ds_count'])
series_3.append(item['tc_count'])
series_1_output = {'name': 'Direct Play',
'data': series_1}
@@ -702,15 +695,15 @@ class Graphs(object):
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_count, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_count, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_count, ' \
'SUM(case when stopped > 0 then (stopped - started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
'FROM session_history ' \
@@ -730,10 +723,10 @@ class Graphs(object):
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
categories.append(item['resolution'])
series_1.append(item['dp_count'])
series_2.append(item['ds_count'])
series_3.append(item['tc_count'])
series_1_output = {'name': 'Direct Play',
'data': series_1}
@@ -780,15 +773,15 @@ class Graphs(object):
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_count, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_count, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_count, ' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
@@ -808,17 +801,10 @@ class Graphs(object):
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
# Rename Mystery platform names
platform_names = [('Mystery 3', 'Playstation 3'),
('Mystery 4', 'Playstation 4'),
('Mystery 5', 'Xbox 360')]
for old_name, new_name in platform_names:
categories = [item.replace(old_name, new_name) for item in categories]
categories.append(common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform']))
series_1.append(item['dp_count'])
series_2.append(item['ds_count'])
series_3.append(item['tc_count'])
series_1_output = {'name': 'Direct Play',
'data': series_1}
@@ -867,15 +853,15 @@ class Graphs(object):
'SUM(case when (session_history_media_info.video_decision = "direct play" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "direct play")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as dp_count, ' \
'SUM(case when (session_history_media_info.video_decision = "copy" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "copy")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as ds_count, ' \
'SUM(case when (session_history_media_info.video_decision = "transcode" ' \
'or (session_history_media_info.video_decision = "" and session_history_media_info.audio_decision = "transcode")) ' \
'and session_history.stopped > 0 then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_duration, ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as tc_count, ' \
'SUM(case when session_history.stopped > 0 ' \
'then (session_history.stopped - session_history.started) ' \
' - (case when paused_counter is NULL then 0 else paused_counter end) else 0 end) as total_duration ' \
@@ -896,10 +882,10 @@ class Graphs(object):
series_3 = []
for item in result:
categories.append(item[0])
series_1.append(item[1])
series_2.append(item[2])
series_3.append(item[3])
categories.append(item['username'])
series_1.append(item['dp_count'])
series_2.append(item['ds_count'])
series_3.append(item['tc_count'])
series_1_output = {'name': 'Direct Play',
'data': series_1}

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
@@ -92,13 +92,15 @@ def latinToAscii(unicrap):
}
r = ''
for i in unicrap:
if ord(i) in xlate:
r += xlate[ord(i)]
elif ord(i) >= 0x80:
pass
else:
r += str(i)
if unicrap:
for i in unicrap:
if ord(i) in xlate:
r += xlate[ord(i)]
elif ord(i) >= 0x80:
pass
else:
r += str(i)
return r
@@ -144,6 +146,35 @@ def now():
now = datetime.datetime.now()
return now.strftime("%Y-%m-%d %H:%M:%S")
def human_duration(s, sig='dhms'):
hd = ''
if str(s).isdigit():
d = int(s / 84600)
h = int((s % 84600) / 3600)
m = int(((s % 84600) % 3600) / 60)
s = int(((s % 84600) % 3600) % 60)
hd_list = []
if sig >= 'd' and d > 0:
d = d + 1 if sig == 'd' and h >= 12 else d
hd_list.append(str(d) + ' days')
if sig >= 'dh' and h > 0:
h = h + 1 if sig == 'dh' and m >= 30 else h
hd_list.append(str(h) + ' hrs')
if sig >= 'dhm' and m > 0:
m = m + 1 if sig == 'dhm' and s >= 30 else m
hd_list.append(str(m) + ' mins')
if sig >= 'dhms' and s > 0:
hd_list.append(str(s) + ' secs')
hd = ' '.join(hd_list)
return hd
def get_age(date):
@@ -177,7 +208,7 @@ def piratesize(size):
split = size.split(" ")
factor = float(split[0])
unit = split[1].upper()
if unit == 'MiB':
size = factor * 1048576
elif unit == 'MB':
@@ -403,3 +434,9 @@ def process_json_kwargs(json_kwargs):
params = json.loads(json_kwargs)
return params
def sanitize(string):
if string:
return unicode(string).replace('<','&lt;').replace('>','&gt;')
else:
return ''

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, config, notifiers, database, helpers
from plexpy import logger, config, notifiers, database, helpers, plextv, pmsconnect
import plexpy
import time
@@ -30,144 +30,191 @@ def notify(stream_data=None, notify_action=None):
if not user_details['do_notify']:
return
if stream_data['media_type'] == 'movie' or stream_data['media_type'] == 'episode':
if plexpy.CONFIG.MOVIE_NOTIFY_ENABLE or plexpy.CONFIG.TV_NOTIFY_ENABLE:
if (stream_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
or (stream_data['media_type'] == 'episode' and plexpy.CONFIG.TV_NOTIFY_ENABLE):
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
progress_percent = helpers.get_percent(stream_data['view_offset'], stream_data['duration'])
for agent in notifiers.available_notification_agents():
if agent['on_play'] and notify_action == 'play':
for agent in notifiers.available_notification_agents():
if agent['on_play'] and notify_action == 'play':
# 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=notify_action, agent_info=agent)
elif agent['on_stop'] and notify_action == 'stop' \
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT):
# 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=notify_action, agent_info=agent)
elif agent['on_pause'] and notify_action == 'pause' \
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
# 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=notify_action, agent_info=agent)
elif agent['on_resume'] and notify_action == 'resume' \
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
# 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=notify_action, 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=notify_action, 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)
# If there is nothing in the notify_log for our agent id but it is enabled we should notify
if not any(d['agent_id'] == agent['id'] for d in notify_states):
# 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='play', agent_info=agent)
set_notify_state(session=stream_data, state=notify_action, agent_info=agent)
elif agent['on_stop'] and notify_action == 'stop' \
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < plexpy.CONFIG.NOTIFY_WATCHED_PERCENT):
# 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])
else:
# Check in our notify log if the notification has already been sent
for notify_state in notify_states:
if not notify_state['on_watched'] and (notify_state['agent_id'] == agent['id']):
# 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=notify_action, agent_info=agent)
set_notify_state(session=stream_data, state='stop', agent_info=agent)
elif (stream_data['media_type'] == 'track' and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
elif agent['on_pause'] and notify_action == 'pause' \
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
# 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])
for agent in notifiers.available_notification_agents():
if agent['on_play'] and notify_action == 'play':
# 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=notify_action, agent_info=agent)
set_notify_state(session=stream_data, state='pause', agent_info=agent)
elif agent['on_stop'] and notify_action == 'stop':
# 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=notify_action, agent_info=agent)
elif agent['on_resume'] and notify_action == 'resume' \
and (plexpy.CONFIG.NOTIFY_CONSECUTIVE or progress_percent < 99):
# 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])
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=notify_action, agent_info=agent)
set_notify_state(session=stream_data, state='resume', 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=notify_action, 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)
# If there is nothing in the notify_log for our agent id but it is enabled we should notify
if not any(d['agent_id'] == agent['id'] for d in notify_states):
# 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='watched', agent_info=agent)
else:
# Check in our notify log if the notification has already been sent
for notify_state in notify_states:
if not notify_state['on_watched'] and (notify_state['agent_id'] == agent['id']):
# 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='watched', agent_info=agent)
elif stream_data['media_type'] == 'track':
if plexpy.CONFIG.MUSIC_NOTIFY_ENABLE:
for agent in notifiers.available_notification_agents():
if agent['on_play'] and notify_action == 'play':
# 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='play', agent_info=agent)
elif agent['on_stop'] and notify_action == 'stop':
# 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='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 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=notify_action, agent_info=agent)
elif stream_data['media_type'] == 'clip':
pass
else:
logger.debug(u"PlexPy Notifier :: Notify called with unsupported media type.")
#logger.debug(u"PlexPy Notifier :: Notify called with unsupported media type.")
pass
else:
logger.debug(u"PlexPy Notifier :: Notify called but incomplete data received.")
def notify_timeline(timeline_data=None, notify_action=None):
if timeline_data and notify_action:
if (timeline_data['media_type'] == 'movie' and plexpy.CONFIG.MOVIE_NOTIFY_ENABLE) \
or ((timeline_data['media_type'] == 'show' or timeline_data['media_type'] == 'episode') \
and plexpy.CONFIG.TV_NOTIFY_ENABLE) \
or ((timeline_data['media_type'] == 'artist' or timeline_data['media_type'] == 'track') \
and plexpy.CONFIG.MUSIC_NOTIFY_ENABLE):
for agent in notifiers.available_notification_agents():
if agent['on_created'] and notify_action == 'created':
# Build and send notification
notify_strings = build_notify_text(timeline=timeline_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=timeline_data, state=notify_action, agent_info=agent)
elif not timeline_data and notify_action:
for agent in notifiers.available_notification_agents():
if agent['on_extdown'] and notify_action == 'extdown':
# Build and send notification
notify_strings = build_server_notify_text(state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
if agent['on_intdown'] and notify_action == 'intdown':
# Build and send notification
notify_strings = build_server_notify_text(state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
if agent['on_extup'] and notify_action == 'extup':
# Build and send notification
notify_strings = build_server_notify_text(state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
if agent['on_intup'] and notify_action == 'intup':
# Build and send notification
notify_strings = build_server_notify_text(state=notify_action)
notifiers.send_notification(config_id=agent['id'],
subject=notify_strings[0],
body=notify_strings[1])
else:
logger.debug(u"PlexPy Notifier :: Notify timeline called but incomplete data received.")
def get_notify_state(session):
monitor_db = database.MonitorDatabase()
result = monitor_db.select('SELECT on_play, on_stop, on_pause, on_resume, on_buffer, on_watched, agent_id '
@@ -179,13 +226,28 @@ def get_notify_state(session):
args=[session['session_key'], session['rating_key'], session['user']])
notify_states = []
for item in result:
notify_state = {'on_play': item[0],
'on_stop': item[1],
'on_pause': item[2],
'on_resume': item[3],
'on_buffer': item[4],
'on_watched': item[5],
'agent_id': item[6]}
notify_state = {'on_play': item['on_play'],
'on_stop': item['on_stop'],
'on_pause': item['on_pause'],
'on_resume': item['on_resume'],
'on_buffer': item['on_buffer'],
'on_watched': item['on_watched'],
'agent_id': item['agent_id']}
notify_states.append(notify_state)
return notify_states
def get_notify_state_timeline(timeline):
monitor_db = database.MonitorDatabase()
result = monitor_db.select('SELECT on_created, agent_id '
'FROM notify_log '
'WHERE rating_key = ? '
'ORDER BY id DESC',
args=[timeline['rating_key']])
notify_states = []
for item in result:
notify_state = {'on_created': item['on_created'],
'agent_id': item['agent_id']}
notify_states.append(notify_state)
return notify_states
@@ -208,53 +270,77 @@ def set_notify_state(session, state, agent_info):
values = {'on_buffer': int(time.time())}
elif state == 'watched':
values = {'on_watched': int(time.time())}
elif state == 'created':
values = {'on_created': int(time.time())}
else:
return
keys = {'session_key': session['session_key'],
'rating_key': session['rating_key'],
'user_id': session['user_id'],
'user': session['user'],
'agent_id': agent_info['id'],
'agent_name': agent_info['name']}
if state == 'created':
keys = {'rating_key': session['rating_key'],
'agent_id': agent_info['id'],
'agent_name': agent_info['name']}
else:
keys = {'session_key': session['session_key'],
'rating_key': session['rating_key'],
'user_id': session['user_id'],
'user': session['user'],
'agent_id': agent_info['id'],
'agent_name': agent_info['name']}
monitor_db.upsert(table_name='notify_log', key_dict=keys, value_dict=values)
else:
logger.error('PlexPy Notifier :: Unable to set notify state.')
def build_notify_text(session, state):
from plexpy import pmsconnect, helpers
def build_notify_text(session=None, timeline=None, state=None):
import re
# Get the server name
pms_connect = pmsconnect.PmsConnect()
server_name = pms_connect.get_server_pref(pref='FriendlyName')
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
if server_times:
updated_at = server_times[0]['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
else:
logger.error(u"PlexPy Notifier :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
# Get metadata feed for item
metadata = pms_connect.get_metadata_details(rating_key=session['rating_key'])
if session:
rating_key = session['rating_key']
elif timeline:
rating_key = timeline['rating_key']
if metadata:
item_metadata = metadata['metadata']
pms_connect = pmsconnect.PmsConnect()
metadata_list = pms_connect.get_metadata_details(rating_key=rating_key)
if metadata_list:
metadata = metadata_list['metadata']
else:
logger.error(u"PlexPy Notifier :: Unable to retrieve metadata for rating_key %s" % str(session['rating_key']))
logger.error(u"PlexPy Notifier :: Unable to retrieve metadata for rating_key %s" % str(rating_key))
return []
# Check for exclusion tags
if session['media_type'] == 'episode':
if metadata['media_type'] == 'movie':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<movie>[^>]+.</movie>|<music>[^>]+.</music>', re.IGNORECASE)
elif session['media_type'] == 'movie':
pattern = re.compile('\n*<tv>[^>]+.</tv>\n*|\n*<music>[^>]+.</music>\n*', re.IGNORECASE|re.DOTALL)
elif metadata['media_type'] == 'show' or metadata['media_type'] == 'episode':
# Regex pattern to remove the text in the tags we don't want
pattern = re.compile('<tv>[^>]+.</tv>|<music>[^>]+.</music>', re.IGNORECASE)
elif session['media_type'] == 'track':
pattern = re.compile('\n*<movie>[^>]+.</movie>\n*|\n*?<music>[^>]+.</music>\n*', re.IGNORECASE|re.DOTALL)
elif metadata['media_type'] == 'artist' or metadata['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)
pattern = re.compile('\n*<tv>[^>]+.</tv>\n*|\n*<movie>[^>]+.</movie>\n*', re.IGNORECASE|re.DOTALL)
else:
pattern = None
if session['media_type'] == 'episode' or session['media_type'] == 'movie' or session['media_type'] == 'track' \
and pattern:
if metadata['media_type'] == 'movie' \
or metadata['media_type'] == 'show' or metadata['media_type'] == 'episode' \
or metadata['media_type'] == 'artist' or metadata['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))
@@ -268,6 +354,8 @@ def build_notify_text(session, state):
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))
on_created_subject = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT))
on_created_body = strip_tag(re.sub(pattern, '', plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT))
else:
on_start_subject = plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT
on_start_body = plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT
@@ -281,68 +369,154 @@ def build_notify_text(session, state):
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
on_created_subject = plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT
on_created_body = plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT
# Create a title
if session['media_type'] == 'episode':
full_title = '%s - %s' % (session['grandparent_title'],
session['title'])
elif session['media_type'] == 'track':
full_title = '%s - %s' % (session['grandparent_title'],
session['title'])
if metadata['media_type'] == 'episode' or metadata['media_type'] == 'track':
full_title = '%s - %s' % (metadata['grandparent_title'],
metadata['title'])
else:
full_title = session['title']
full_title = metadata['title']
# Generate a combined transcode decision value
if session['video_decision']:
if session['video_decision'] == 'transcode':
duration = helpers.convert_milliseconds_to_minutes(metadata['duration'])
# Default values
user = ''
platform = ''
player = ''
ip_address = 'N/A'
stream_duration = 0
view_offset = 0
container = ''
video_codec = ''
video_bitrate = ''
video_width = ''
video_height = ''
video_resolution = ''
video_framerate = ''
aspect_ratio = ''
audio_codec = ''
audio_channels = ''
transcode_decision = ''
video_decision = ''
audio_decision = ''
transcode_container = ''
transcode_video_codec = ''
transcode_video_width = ''
transcode_video_height = ''
transcode_audio_codec = ''
transcode_audio_channels = ''
# Session values
if session:
# Generate a combined transcode decision value
video_decision = session['video_decision'].title()
audio_decision = session['audio_decision'].title()
if session['video_decision'] == 'transcode' or session['audio_decision'] == 'transcode':
transcode_decision = 'Transcode'
elif session['video_decision'] == 'copy' or session['audio_decision'] == 'copy':
transcode_decision = 'Direct Stream'
else:
transcode_decision = 'Direct Play'
else:
if session['audio_decision'] == 'transcode':
transcode_decision = 'Transcode'
else:
transcode_decision = 'Direct Play'
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)
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)
view_offset = helpers.convert_milliseconds_to_minutes(session['view_offset'])
user = session['friendly_name']
platform = session['platform']
player = session['player']
ip_address = session['ip_address'] if session['ip_address'] else 'N/A'
container = session['container']
video_codec = session['video_codec']
video_bitrate = session['bitrate']
video_width = session['width']
video_height = session['height']
video_resolution = session['video_resolution']
video_framerate = session['video_framerate']
aspect_ratio = session['aspect_ratio']
audio_codec = session['audio_codec']
audio_channels = session['audio_channels']
transcode_container = session['transcode_container']
transcode_video_codec = session['transcode_video_codec']
transcode_video_width = session['transcode_width']
transcode_video_height = session['transcode_height']
transcode_audio_codec = session['transcode_audio_codec']
transcode_audio_channels = session['transcode_audio_channels']
progress_percent = helpers.get_percent(view_offset, duration)
# Fix metadata params for notify recently added grandparent
if plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT:
show_name = metadata['title']
episode_name = ''
artist_name = metadata['title']
album_name = ''
track_name = ''
else:
show_name = metadata['grandparent_title']
episode_name = metadata['title']
artist_name = metadata['grandparent_title']
album_name = metadata['parent_title']
track_name = metadata['title']
available_params = {'server_name': server_name,
'user': session['friendly_name'],
'platform': session['platform'],
'player': session['player'],
'media_type': session['media_type'],
'title': full_title,
'show_name': item_metadata['grandparent_title'],
'episode_name': item_metadata['title'],
'artist_name': item_metadata['grandparent_title'],
'album_name': item_metadata['parent_title'],
'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),
'transcode_decision': transcode_decision,
'year': item_metadata['year'],
'studio': item_metadata['studio'],
'content_rating': item_metadata['content_rating'],
'summary': item_metadata['summary'],
'rating': item_metadata['rating'],
'duration': duration,
'server_uptime': server_uptime,
'user': user,
'platform': platform,
'player': player,
'ip_address': ip_address,
'media_type': metadata['media_type'],
'stream_duration': stream_duration,
'remaining_duration': duration - view_offset,
'progress': view_offset,
'progress_percent': progress_percent
'progress_percent': progress_percent,
'container': container,
'video_codec': video_codec,
'video_bitrate': video_bitrate,
'video_width': video_width,
'video_height': video_height,
'video_resolution': video_resolution,
'video_framerate': video_framerate,
'aspect_ratio': aspect_ratio,
'audio_codec': audio_codec,
'audio_channels': audio_channels,
'transcode_decision': transcode_decision,
'video_decision': video_decision,
'audio_decision': audio_decision,
'transcode_container': transcode_container,
'transcode_video_codec': transcode_video_codec,
'transcode_video_width': transcode_video_width,
'transcode_video_height': transcode_video_height,
'transcode_audio_codec': transcode_audio_codec,
'transcode_audio_channels': transcode_audio_channels,
'title': full_title,
'show_name': show_name,
'episode_name': episode_name,
'artist_name': artist_name,
'album_name': album_name,
'track_name': track_name,
'season_num': metadata['parent_index'].zfill(1),
'season_num00': metadata['parent_index'].zfill(2),
'episode_num': metadata['index'].zfill(1),
'episode_num00': metadata['index'].zfill(2),
'year': metadata['year'],
'studio': metadata['studio'],
'content_rating': metadata['content_rating'],
'directors': ', '.join(metadata['directors']),
'writers': ', '.join(metadata['writers']),
'actors': ', '.join(metadata['actors']),
'genres': ', '.join(metadata['genres']),
'summary': metadata['summary'],
'tagline': metadata['tagline'],
'rating': metadata['rating'],
'duration': duration
}
# Default subject text
@@ -489,12 +663,154 @@ def build_notify_text(session, state):
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 == 'created':
# Default body text
body_text = '%s was recently added to Plex.' % full_title
if on_created_subject and on_created_body:
try:
subject_text = unicode(on_created_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_created_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]
else:
return None
def build_server_notify_text(state=None):
# Get the server name
server_name = plexpy.CONFIG.PMS_NAME
# Get the server uptime
plex_tv = plextv.PlexTV()
server_times = plex_tv.get_server_times()
if server_times:
updated_at = server_times[0]['updated_at']
server_uptime = helpers.human_duration(int(time.time() - helpers.cast_to_float(updated_at)))
else:
logger.error(u"PlexPy Notifier :: Unable to retrieve server uptime.")
server_uptime = 'N/A'
on_extdown_subject = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT
on_extdown_body = plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT
on_intdown_subject = plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT
on_intdown_body = plexpy.CONFIG.NOTIFY_ON_INTDOWN_BODY_TEXT
on_extup_subject = plexpy.CONFIG.NOTIFY_ON_EXTUP_SUBJECT_TEXT
on_extup_body = plexpy.CONFIG.NOTIFY_ON_EXTUP_BODY_TEXT
on_intup_subject = plexpy.CONFIG.NOTIFY_ON_INTUP_SUBJECT_TEXT
on_intup_body = plexpy.CONFIG.NOTIFY_ON_INTUP_BODY_TEXT
available_params = {'server_name': server_name,
'server_uptime': server_uptime}
# Default text
subject_text = 'PlexPy (%s)' % server_name
if state == 'extdown':
# Default body text
body_text = 'The Plex Media Server remote access is down.'
if on_extdown_subject and on_extdown_body:
try:
subject_text = unicode(on_extdown_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_extdown_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 == 'intdown':
# Default body text
body_text = 'The Plex Media Server is down.'
if on_intdown_subject and on_intdown_body:
try:
subject_text = unicode(on_intdown_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_intdown_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]
if state == 'extup':
# Default body text
body_text = 'The Plex Media Server remote access is back up.'
if on_extup_subject and on_extup_body:
try:
subject_text = unicode(on_extup_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_extup_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 == 'intup':
# Default body text
body_text = 'The Plex Media Server is back up.'
if on_intup_subject and on_intup_body:
try:
subject_text = unicode(on_intup_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_intup_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]
else:
return None
def strip_tag(data):
import re

View File

@@ -50,7 +50,9 @@ AGENT_IDS = {"Growl": 0,
"OSX Notify": 8,
"Boxcar2": 9,
"Email": 10,
"Twitter": 11}
"Twitter": 11,
"IFTTT": 12,
"Telegram": 13}
def available_notification_agents():
agents = [{'name': 'Growl',
@@ -63,7 +65,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.GROWL_ON_WATCHED,
'on_created': plexpy.CONFIG.GROWL_ON_CREATED,
'on_extdown': plexpy.CONFIG.GROWL_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.GROWL_ON_INTDOWN,
'on_extup': plexpy.CONFIG.GROWL_ON_EXTUP,
'on_intup': plexpy.CONFIG.GROWL_ON_INTUP
},
{'name': 'Prowl',
'id': AGENT_IDS['Prowl'],
@@ -75,7 +82,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.PROWL_ON_WATCHED,
'on_created': plexpy.CONFIG.PROWL_ON_CREATED,
'on_extdown': plexpy.CONFIG.PROWL_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PROWL_ON_INTDOWN,
'on_extup': plexpy.CONFIG.PROWL_ON_EXTUP,
'on_intup': plexpy.CONFIG.PROWL_ON_INTUP
},
{'name': 'XBMC',
'id': AGENT_IDS['XBMC'],
@@ -87,7 +99,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.XBMC_ON_WATCHED,
'on_created': plexpy.CONFIG.XBMC_ON_CREATED,
'on_extdown': plexpy.CONFIG.XBMC_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.XBMC_ON_INTDOWN,
'on_extup': plexpy.CONFIG.XBMC_ON_EXTUP,
'on_intup': plexpy.CONFIG.XBMC_ON_INTUP
},
{'name': 'Plex',
'id': AGENT_IDS['Plex'],
@@ -99,7 +116,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.PLEX_ON_WATCHED,
'on_created': plexpy.CONFIG.PLEX_ON_CREATED,
'on_extdown': plexpy.CONFIG.PLEX_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PLEX_ON_INTDOWN,
'on_extup': plexpy.CONFIG.PLEX_ON_EXTUP,
'on_intup': plexpy.CONFIG.PLEX_ON_INTUP
},
{'name': 'NotifyMyAndroid',
'id': AGENT_IDS['NMA'],
@@ -111,7 +133,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.NMA_ON_WATCHED,
'on_created': plexpy.CONFIG.NMA_ON_CREATED,
'on_extdown': plexpy.CONFIG.NMA_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.NMA_ON_INTDOWN,
'on_extup': plexpy.CONFIG.NMA_ON_EXTUP,
'on_intup': plexpy.CONFIG.NMA_ON_INTUP
},
{'name': 'Pushalot',
'id': AGENT_IDS['Pushalot'],
@@ -123,7 +150,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.PUSHALOT_ON_WATCHED,
'on_created': plexpy.CONFIG.PUSHALOT_ON_CREATED,
'on_extdown': plexpy.CONFIG.PUSHALOT_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PUSHALOT_ON_INTDOWN,
'on_extup': plexpy.CONFIG.PUSHALOT_ON_EXTUP,
'on_intup': plexpy.CONFIG.PUSHALOT_ON_INTUP
},
{'name': 'Pushbullet',
'id': AGENT_IDS['Pushbullet'],
@@ -135,7 +167,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.PUSHBULLET_ON_WATCHED,
'on_created': plexpy.CONFIG.PUSHBULLET_ON_CREATED,
'on_extdown': plexpy.CONFIG.PUSHBULLET_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PUSHBULLET_ON_INTDOWN,
'on_extup': plexpy.CONFIG.PUSHBULLET_ON_EXTUP,
'on_intup': plexpy.CONFIG.PUSHBULLET_ON_INTUP
},
{'name': 'Pushover',
'id': AGENT_IDS['Pushover'],
@@ -147,7 +184,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.PUSHOVER_ON_WATCHED,
'on_created': plexpy.CONFIG.PUSHOVER_ON_CREATED,
'on_extdown': plexpy.CONFIG.PUSHOVER_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.PUSHOVER_ON_INTDOWN,
'on_extup': plexpy.CONFIG.PUSHOVER_ON_EXTUP,
'on_intup': plexpy.CONFIG.PUSHOVER_ON_INTUP
},
{'name': 'Boxcar2',
'id': AGENT_IDS['Boxcar2'],
@@ -159,7 +201,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.BOXCAR_ON_WATCHED,
'on_created': plexpy.CONFIG.BOXCAR_ON_CREATED,
'on_extdown': plexpy.CONFIG.BOXCAR_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.BOXCAR_ON_INTDOWN,
'on_extup': plexpy.CONFIG.BOXCAR_ON_EXTUP,
'on_intup': plexpy.CONFIG.BOXCAR_ON_INTUP
},
{'name': 'E-mail',
'id': AGENT_IDS['Email'],
@@ -171,7 +218,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.EMAIL_ON_WATCHED,
'on_created': plexpy.CONFIG.EMAIL_ON_CREATED,
'on_extdown': plexpy.CONFIG.EMAIL_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.EMAIL_ON_INTDOWN,
'on_extup': plexpy.CONFIG.EMAIL_ON_EXTUP,
'on_intup': plexpy.CONFIG.EMAIL_ON_INTUP
},
{'name': 'Twitter',
'id': AGENT_IDS['Twitter'],
@@ -183,7 +235,46 @@ def available_notification_agents():
'on_pause': plexpy.CONFIG.TWITTER_ON_PAUSE,
'on_resume': plexpy.CONFIG.TWITTER_ON_RESUME,
'on_buffer': plexpy.CONFIG.TWITTER_ON_BUFFER,
'on_watched': plexpy.CONFIG.TWITTER_ON_WATCHED
'on_watched': plexpy.CONFIG.TWITTER_ON_WATCHED,
'on_created': plexpy.CONFIG.TWITTER_ON_CREATED,
'on_extdown': plexpy.CONFIG.TWITTER_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.TWITTER_ON_INTDOWN,
'on_extup': plexpy.CONFIG.TWITTER_ON_EXTUP,
'on_intup': plexpy.CONFIG.TWITTER_ON_INTUP
},
{'name': 'IFTTT',
'id': AGENT_IDS['IFTTT'],
'config_prefix': 'ifttt',
'has_config': True,
'state': checked(plexpy.CONFIG.IFTTT_ENABLED),
'on_play': plexpy.CONFIG.IFTTT_ON_PLAY,
'on_stop': plexpy.CONFIG.IFTTT_ON_STOP,
'on_pause': plexpy.CONFIG.IFTTT_ON_PAUSE,
'on_resume': plexpy.CONFIG.IFTTT_ON_RESUME,
'on_buffer': plexpy.CONFIG.IFTTT_ON_BUFFER,
'on_watched': plexpy.CONFIG.IFTTT_ON_WATCHED,
'on_created': plexpy.CONFIG.IFTTT_ON_CREATED,
'on_extdown': plexpy.CONFIG.IFTTT_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.IFTTT_ON_INTDOWN,
'on_extup': plexpy.CONFIG.IFTTT_ON_EXTUP,
'on_intup': plexpy.CONFIG.IFTTT_ON_INTUP
},
{'name': 'Telegram',
'id': AGENT_IDS['Telegram'],
'config_prefix': 'telegram',
'has_config': True,
'state': checked(plexpy.CONFIG.TELEGRAM_ENABLED),
'on_play': plexpy.CONFIG.TELEGRAM_ON_PLAY,
'on_stop': plexpy.CONFIG.TELEGRAM_ON_STOP,
'on_pause': plexpy.CONFIG.TELEGRAM_ON_PAUSE,
'on_resume': plexpy.CONFIG.TELEGRAM_ON_RESUME,
'on_buffer': plexpy.CONFIG.TELEGRAM_ON_BUFFER,
'on_watched': plexpy.CONFIG.TELEGRAM_ON_WATCHED,
'on_created': plexpy.CONFIG.TELEGRAM_ON_CREATED,
'on_extdown': plexpy.CONFIG.TELEGRAM_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.TELEGRAM_ON_INTDOWN,
'on_extup': plexpy.CONFIG.TELEGRAM_ON_EXTUP,
'on_intup': plexpy.CONFIG.TELEGRAM_ON_INTUP
}
]
@@ -200,7 +291,12 @@ def available_notification_agents():
'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
'on_watched': plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED,
'on_created': plexpy.CONFIG.OSX_NOTIFY_ON_CREATED,
'on_extdown': plexpy.CONFIG.OSX_NOTIFY_ON_EXTDOWN,
'on_intdown': plexpy.CONFIG.OSX_NOTIFY_ON_INTDOWN,
'on_extup': plexpy.CONFIG.OSX_NOTIFY_ON_EXTUP,
'on_intup': plexpy.CONFIG.OSX_NOTIFY_ON_INTUP
})
return agents
@@ -245,13 +341,19 @@ def get_notification_agent_config(config_id):
elif config_id == 11:
tweet = TwitterNotifier()
return tweet.return_config_options()
elif config_id == 12:
iftttClient = IFTTT()
return iftttClient.return_config_options()
elif config_id == 13:
telegramClient = TELEGRAM()
return telegramClient.return_config_options()
else:
return []
else:
return []
def send_notification(config_id, subject, body):
if config_id:
if str(config_id).isdigit():
config_id = int(config_id)
if config_id == 0:
@@ -290,6 +392,12 @@ def send_notification(config_id, subject, body):
elif config_id == 11:
tweet = TwitterNotifier()
tweet.notify(subject=subject, message=body)
elif config_id == 12:
iftttClient = IFTTT()
iftttClient.notify(subject=subject, message=body)
elif config_id == 13:
telegramClient = TELEGRAM()
telegramClient.notify(message=body, event=subject)
else:
logger.debug(u"PlexPy Notifier :: Unknown agent id received.")
else:
@@ -305,9 +413,6 @@ class GROWL(object):
self.enabled = plexpy.CONFIG.GROWL_ENABLED
self.host = plexpy.CONFIG.GROWL_HOST
self.password = plexpy.CONFIG.GROWL_PASSWORD
self.on_play = plexpy.CONFIG.GROWL_ON_PLAY
self.on_stop = plexpy.CONFIG.GROWL_ON_STOP
self.on_watched = plexpy.CONFIG.GROWL_ON_WATCHED
def conf(self, options):
return cherrypy.config['config'].get('Growl', options)
@@ -355,7 +460,7 @@ class GROWL(object):
# Send it, including an image
image_file = os.path.join(str(plexpy.PROG_DIR),
"data/images/plexpylogo.png")
"data/interfaces/default/images/favicon.png")
with open(image_file, 'rb') as f:
image = f.read()
@@ -410,9 +515,6 @@ class PROWL(object):
self.enabled = plexpy.CONFIG.PROWL_ENABLED
self.keys = plexpy.CONFIG.PROWL_KEYS
self.priority = plexpy.CONFIG.PROWL_PRIORITY
self.on_play = plexpy.CONFIG.PROWL_ON_PLAY
self.on_stop = plexpy.CONFIG.PROWL_ON_STOP
self.on_watched = plexpy.CONFIG.PROWL_ON_WATCHED
def conf(self, options):
return cherrypy.config['config'].get('Prowl', options)
@@ -425,7 +527,7 @@ class PROWL(object):
data = {'apikey': plexpy.CONFIG.PROWL_KEYS,
'application': 'PlexPy',
'event': event,
'event': event.encode("utf-8"),
'description': message.encode("utf-8"),
'priority': plexpy.CONFIG.PROWL_PRIORITY}
@@ -485,9 +587,6 @@ class XBMC(object):
self.hosts = plexpy.CONFIG.XBMC_HOST
self.username = plexpy.CONFIG.XBMC_USERNAME
self.password = plexpy.CONFIG.XBMC_PASSWORD
self.on_play = plexpy.CONFIG.XBMC_ON_PLAY
self.on_stop = plexpy.CONFIG.XBMC_ON_STOP
self.on_watched = plexpy.CONFIG.XBMC_ON_WATCHED
def _sendhttp(self, host, command):
url_command = urllib.urlencode(command)
@@ -568,9 +667,6 @@ class Plex(object):
self.client_hosts = plexpy.CONFIG.PLEX_CLIENT_HOST
self.username = plexpy.CONFIG.PLEX_USERNAME
self.password = plexpy.CONFIG.PLEX_PASSWORD
self.on_play = plexpy.CONFIG.PLEX_ON_PLAY
self.on_stop = plexpy.CONFIG.PLEX_ON_STOP
self.on_watched = plexpy.CONFIG.PLEX_ON_WATCHED
def _sendhttp(self, host, command):
@@ -648,9 +744,6 @@ class NMA(object):
def __init__(self):
self.api = plexpy.CONFIG.NMA_APIKEY
self.nma_priority = plexpy.CONFIG.NMA_PRIORITY
self.on_play = plexpy.CONFIG.NMA_ON_PLAY
self.on_stop = plexpy.CONFIG.NMA_ON_STOP
self.on_watched = plexpy.CONFIG.NMA_ON_WATCHED
def notify(self, subject=None, message=None):
if not subject or not message:
@@ -710,9 +803,6 @@ class PUSHBULLET(object):
self.apikey = plexpy.CONFIG.PUSHBULLET_APIKEY
self.deviceid = plexpy.CONFIG.PUSHBULLET_DEVICEID
self.channel_tag = plexpy.CONFIG.PUSHBULLET_CHANNEL_TAG
self.on_play = plexpy.CONFIG.PUSHBULLET_ON_PLAY
self.on_stop = plexpy.CONFIG.PUSHBULLET_ON_STOP
self.on_watched = plexpy.CONFIG.PUSHBULLET_ON_WATCHED
def conf(self, options):
return cherrypy.config['config'].get('PUSHBULLET', options)
@@ -762,6 +852,31 @@ class PUSHBULLET(object):
self.notify('Main Screen Activate', 'Test Message')
def get_devices(self):
if plexpy.CONFIG.PUSHBULLET_APIKEY:
http_handler = HTTPSConnection("api.pushbullet.com")
http_handler.request("GET", "/v2/devices",
headers={'Content-type': "application/json",
'Authorization': 'Basic %s' % base64.b64encode(plexpy.CONFIG.PUSHBULLET_APIKEY + ":")})
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
data = json.loads(response.read())
devices = data.get('devices', [])
devices = {d['iden']: d['nickname'] for d in devices if d['active']}
devices.update({'': ''})
return devices
elif request_status >= 400 and request_status < 500:
logger.info(u"Unable to retrieve Pushbullet devices list: %s" % response.reason)
return {'': ''}
else:
logger.info(u"Unable to retrieve Pushbullet devices list.")
return {'': ''}
else:
return {'': ''}
def return_config_options(self):
config_option = [{'label': 'Pushbullet API Key',
'value': self.apikey,
@@ -769,11 +884,13 @@ class PUSHBULLET(object):
'description': 'Your Pushbullet API key.',
'input_type': 'text'
},
{'label': 'Device ID',
{'label': 'Device',
'value': self.deviceid,
'name': 'pushbullet_deviceid',
'description': 'A device ID (optional). If set, will override channel tag.',
'input_type': 'text'
'description': 'Set your Pushbullet device. If set, will override channel tag. ' \
'Leave blank to notify on all devices.',
'input_type': 'select',
'select_options': self.get_devices()
},
{'label': 'Channel',
'value': self.channel_tag,
@@ -789,9 +906,6 @@ class PUSHALOT(object):
def __init__(self):
self.api_key = plexpy.CONFIG.PUSHALOT_APIKEY
self.on_play = plexpy.CONFIG.PUSHALOT_ON_PLAY
self.on_stop = plexpy.CONFIG.PUSHALOT_ON_STOP
self.on_watched = plexpy.CONFIG.PUSHALOT_ON_WATCHED
def notify(self, message, event):
if not message or not event:
@@ -799,9 +913,9 @@ class PUSHALOT(object):
pushalot_authorizationtoken = plexpy.CONFIG.PUSHALOT_APIKEY
logger.debug(u"Pushalot event: " + event)
logger.debug(u"Pushalot message: " + message)
logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
#logger.debug(u"Pushalot event: " + event)
#logger.debug(u"Pushalot message: " + message)
#logger.debug(u"Pushalot api: " + pushalot_authorizationtoken)
http_handler = HTTPSConnection("pushalot.com")
@@ -816,9 +930,9 @@ class PUSHALOT(object):
response = http_handler.getresponse()
request_status = response.status
logger.debug(u"Pushalot response status: %r" % request_status)
logger.debug(u"Pushalot response headers: %r" % response.getheaders())
logger.debug(u"Pushalot response body: %r" % response.read())
#logger.debug(u"Pushalot response status: %r" % request_status)
#logger.debug(u"Pushalot response headers: %r" % response.getheaders())
#logger.debug(u"Pushalot response body: %r" % response.read())
if request_status == 200:
logger.info(u"Pushalot notifications sent.")
@@ -848,9 +962,6 @@ class PUSHOVER(object):
self.keys = plexpy.CONFIG.PUSHOVER_KEYS
self.priority = plexpy.CONFIG.PUSHOVER_PRIORITY
self.sound = plexpy.CONFIG.PUSHOVER_SOUND
self.on_play = plexpy.CONFIG.PUSHOVER_ON_PLAY
self.on_stop = plexpy.CONFIG.PUSHOVER_ON_STOP
self.on_watched = plexpy.CONFIG.PUSHOVER_ON_WATCHED
if plexpy.CONFIG.PUSHOVER_APITOKEN:
self.application_token = plexpy.CONFIG.PUSHOVER_APITOKEN
@@ -868,7 +979,7 @@ class PUSHOVER(object):
data = {'token': self.application_token,
'user': plexpy.CONFIG.PUSHOVER_KEYS,
'title': event,
'title': event.encode("utf-8"),
'message': message.encode("utf-8"),
'sound': plexpy.CONFIG.PUSHOVER_SOUND,
'priority': plexpy.CONFIG.PUSHOVER_PRIORITY}
@@ -924,10 +1035,10 @@ class PUSHOVER(object):
return {'': ''}
def return_config_options(self):
config_option = [{'label': 'Pushover User Key',
config_option = [{'label': 'Pushover User or Group Key',
'value': self.keys,
'name': 'pushover_keys',
'description': 'Your Pushover user key.',
'description': 'Your Pushover user or group key.',
'input_type': 'text'
},
{'label': 'Priority',
@@ -1078,9 +1189,6 @@ class TwitterNotifier(object):
class OSX_NOTIFY(object):
def __init__(self):
self.on_play = plexpy.CONFIG.OSX_NOTIFY_ON_PLAY
self.on_stop = plexpy.CONFIG.OSX_NOTIFY_ON_STOP
self.on_watched = plexpy.CONFIG.OSX_NOTIFY_ON_WATCHED
try:
self.objc = __import__("objc")
self.AppKit = __import__("AppKit")
@@ -1167,9 +1275,6 @@ class BOXCAR(object):
self.url = 'https://new.boxcar.io/api/notifications'
self.token = plexpy.CONFIG.BOXCAR_TOKEN
self.sound = plexpy.CONFIG.BOXCAR_SOUND
self.on_play = plexpy.CONFIG.BOXCAR_ON_PLAY
self.on_stop = plexpy.CONFIG.BOXCAR_ON_STOP
self.on_watched = plexpy.CONFIG.BOXCAR_ON_WATCHED
def notify(self, title, message):
if not title or not message:
@@ -1186,6 +1291,7 @@ class BOXCAR(object):
req = urllib2.Request(self.url)
handle = urllib2.urlopen(req, data)
handle.close()
logger.info(u"Boxcar2 notifications sent.")
return True
except urllib2.URLError as e:
@@ -1242,9 +1348,7 @@ class BOXCAR(object):
class Email(object):
def __init__(self):
self.on_play = plexpy.CONFIG.EMAIL_ON_PLAY
self.on_stop = plexpy.CONFIG.EMAIL_ON_STOP
self.on_watched = plexpy.CONFIG.EMAIL_ON_WATCHED
pass
def notify(self, subject, message):
if not subject or not message:
@@ -1254,6 +1358,12 @@ class Email(object):
message['Subject'] = subject
message['From'] = email.utils.formataddr(('PlexPy', plexpy.CONFIG.EMAIL_FROM))
message['To'] = plexpy.CONFIG.EMAIL_TO
message['CC'] = plexpy.CONFIG.EMAIL_CC
recipients = [x.strip() for x in plexpy.CONFIG.EMAIL_TO.split(';')] \
+ [x.strip() for x in plexpy.CONFIG.EMAIL_CC.split(';')] \
+ [x.strip() for x in plexpy.CONFIG.EMAIL_BCC.split(';')]
recipients = filter(None, recipients)
try:
mailserver = smtplib.SMTP(plexpy.CONFIG.EMAIL_SMTP_SERVER, plexpy.CONFIG.EMAIL_SMTP_PORT)
@@ -1266,8 +1376,10 @@ class Email(object):
if plexpy.CONFIG.EMAIL_SMTP_USER:
mailserver.login(plexpy.CONFIG.EMAIL_SMTP_USER, plexpy.CONFIG.EMAIL_SMTP_PASSWORD)
mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, plexpy.CONFIG.EMAIL_TO, message.as_string())
mailserver.sendmail(plexpy.CONFIG.EMAIL_FROM, recipients, message.as_string())
mailserver.quit()
logger.info(u"Email notifications sent.")
return True
except Exception, e:
@@ -1278,13 +1390,25 @@ class Email(object):
config_option = [{'label': 'From',
'value': plexpy.CONFIG.EMAIL_FROM,
'name': 'email_from',
'description': 'Who should the sender be.',
'description': 'The email address of the sender.',
'input_type': 'text'
},
{'label': 'To',
'value': plexpy.CONFIG.EMAIL_TO,
'name': 'email_to',
'description': 'Who should the recipient be.',
'description': 'The email address(es) of the recipients, separated by semicolons (;).',
'input_type': 'text'
},
{'label': 'CC',
'value': plexpy.CONFIG.EMAIL_CC,
'name': 'email_cc',
'description': 'The email address(es) to CC, separated by semicolons (;).',
'input_type': 'text'
},
{'label': 'BCC',
'value': plexpy.CONFIG.EMAIL_BCC,
'name': 'email_bcc',
'description': 'The email address(es) to BCC, separated by semicolons (;).',
'input_type': 'text'
},
{'label': 'SMTP Server',
@@ -1319,4 +1443,134 @@ class Email(object):
}
]
return config_option
return config_option
class IFTTT(object):
def __init__(self):
self.apikey = plexpy.CONFIG.IFTTT_KEY
self.event = plexpy.CONFIG.IFTTT_EVENT
def notify(self, message, subject):
if not message or not subject:
return
http_handler = HTTPSConnection("maker.ifttt.com")
data = {'value1': subject.encode("utf-8"),
'value2': message.encode("utf-8")}
# logger.debug("Ifttt SENDING: %s" % json.dumps(data))
http_handler.request("POST",
"/trigger/%s/with/key/%s" % (self.event, self.apikey),
headers={'Content-type': "application/json"},
body=json.dumps(data))
response = http_handler.getresponse()
request_status = response.status
# logger.debug(u"Ifttt response status: %r" % request_status)
# logger.debug(u"Ifttt response headers: %r" % response.getheaders())
# logger.debug(u"Ifttt response body: %r" % response.read())
if request_status == 200:
logger.info(u"Ifttt notifications sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.info(u"Ifttt request failed: %s" % response.reason)
return False
else:
logger.info(u"Ifttt notification failed serverside.")
return False
def test(self):
return self.notify('PlexPy', 'Test Message')
def return_config_options(self):
config_option = [{'label': 'Ifttt Maker Channel Key',
'value': self.apikey,
'name': 'ifttt_key',
'description': 'Your Ifttt key. You can get a key from <a href="https://ifttt.com/maker" target="_blank">here</a>.',
'input_type': 'text'
},
{'label': 'Ifttt Event',
'value': self.event,
'name': 'ifttt_event',
'description': 'The Ifttt maker event to fire. The notification subject and body will be sent'
' as value1 and value2 respectively.',
'input_type': 'text'
},
{'label': 'Test Event',
'value': 'Test Event',
'name': 'testIFTTT',
'description': 'Test if IFTTT notifications are working. See logs for troubleshooting.',
'input_type': 'button'
}
]
return config_option
class TELEGRAM(object):
def __init__(self):
self.enabled = plexpy.CONFIG.TELEGRAM_ENABLED
self.bot_token = plexpy.CONFIG.TELEGRAM_BOT_TOKEN
self.chat_id = plexpy.CONFIG.TELEGRAM_CHAT_ID
def conf(self, options):
return cherrypy.config['config'].get('Telegram', options)
def notify(self, message, event):
if not message or not event:
return
http_handler = HTTPSConnection("api.telegram.org")
data = {'chat_id': self.chat_id,
'text': event.encode('utf-8') + ': ' + message.encode("utf-8")}
http_handler.request("POST",
"/bot%s/%s" % (self.bot_token, "sendMessage"),
headers={'Content-type': "application/x-www-form-urlencoded"},
body=urlencode(data))
response = http_handler.getresponse()
request_status = response.status
if request_status == 200:
logger.info(u"Telegram notifications sent.")
return True
elif request_status >= 400 and request_status < 500:
logger.info(u"Telegram request failed: %s" % response.reason)
return False
else:
logger.info(u"Telegram notification failed serverside.")
return False
def updateLibrary(self):
#For uniformity reasons not removed
return
def test(self, bot_token, chat_id):
self.enabled = True
self.bot_token = bot_token
self.chat_id = chat_id
self.notify('Main Screen Activate', 'Test Message')
def return_config_options(self):
config_option = [{'label': 'Telegram Bot Token',
'value': self.bot_token,
'name': 'telegram_bot_token',
'description': 'Your Telegram bot token. Contact <a href="http://telegram.me/BotFather" target="_blank">@BotFather</a> on Telegram to get one.',
'input_type': 'text'
},
{'label': 'Telegram Chat ID',
'value': self.chat_id,
'name': 'telegram_chat_id',
'description': 'Your Telegram Chat ID, Group ID, or channel username. Contact <a href="http://telegram.me/myidbot" target="_blank">@myidbot</a> on Telegram to get an ID.',
'input_type': 'text'
}
]
return config_option

View File

@@ -342,13 +342,13 @@ class PlexTV(object):
rating_key = clean_uri.rpartition('%2F')[-1]
sync_details = {"device_name": device_name,
"platform": device_platform,
"username": device_username,
"friendly_name": device_friendly_name,
sync_details = {"device_name": helpers.sanitize(device_name),
"platform": helpers.sanitize(device_platform),
"username": helpers.sanitize(device_username),
"friendly_name": helpers.sanitize(device_friendly_name),
"user_id": device_user_id,
"root_title": sync_root_title,
"title": sync_title,
"root_title": helpers.sanitize(sync_root_title),
"title": helpers.sanitize(sync_title),
"metadata_type": sync_metadata_type,
"content_type": sync_content_type,
"rating_key": rating_key,
@@ -444,3 +444,22 @@ class PlexTV(object):
return clean_servers
return json.dumps(clean_servers, indent=4)
def get_server_times(self):
servers = self.get_plextv_server_list(output_format='xml')
server_times = []
try:
xml_head = servers.getElementsByTagName('Server')
except:
logger.warn("Error parsing XML for Plex servers.")
return []
for a in xml_head:
if helpers.get_xml_attr(a, 'machineIdentifier') == plexpy.CONFIG.PMS_IDENTIFIER:
server_times.append({"created_at": helpers.get_xml_attr(a, 'createdAt'),
"updated_at": helpers.get_xml_attr(a, 'updatedAt')
})
break
return server_times

View File

@@ -1,4 +1,4 @@
# This file is part of PlexPy.
# This file is part of PlexPy.
#
# PlexPy is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -247,6 +247,8 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
logger.debug(u"PlexPy Importer :: Disabling monitoring while import in progress.")
plexpy.schedule_job(activity_pinger.check_active_sessions, 'Check for active sessions',
hours=0, minutes=0, seconds=0)
plexpy.schedule_job(activity_pinger.check_recently_added, 'Check for recently added items',
hours=0, minutes=0, seconds=0)
ap = activity_processor.ActivityProcessor()
user_data = users.Users()
@@ -290,10 +292,15 @@ def import_from_plexwatch(database=None, table_name=None, import_ignore_interval
# If we get back None from our xml extractor skip over the record and log error.
if not extracted_xml:
logger.error(u"PlexPy Importer :: Skipping line with ratingKey %s due to malformed xml."
logger.error(u"PlexPy Importer :: Skipping record with ratingKey %s due to malformed xml."
% str(row['rating_key']))
continue
# Skip line if we don't have a ratingKey to work with
if not row['rating_key']:
logger.error(u"PlexPy Importer :: Skipping record due to null ratingRey.")
continue
# If the user_id no longer exists in the friends list, pull it from the xml.
if user_data.get_user_id(user=row['user']):
user_id = user_data.get_user_id(user=row['user'])

View File

@@ -13,12 +13,29 @@
# 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, users, http_handler
from plexpy import logger, helpers, users, http_handler, common
from urlparse import urlparse
import plexpy
import urllib2
def get_server_friendly_name():
logger.info("Requesting name from server...")
server_name = PmsConnect().get_server_pref(pref='FriendlyName')
# If friendly name is blank
if not server_name:
servers_info = PmsConnect().get_servers_info()
for server in servers_info:
if server['machine_identifier'] == plexpy.CONFIG.PMS_IDENTIFIER:
server_name = server['name']
break
if server_name and server_name != plexpy.CONFIG.PMS_NAME:
plexpy.CONFIG.__setattr__('PMS_NAME', server_name)
plexpy.CONFIG.write()
return server_name
class PmsConnect(object):
"""
@@ -254,6 +271,36 @@ class PmsConnect(object):
return request
"""
Return account details.
Optional parameters: output_format { dict, json }
Output: array
"""
def get_account(self, output_format=''):
uri = '/myplex/account'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='GET',
output_format=output_format)
return request
"""
Refresh Plex remote access port mapping.
Optional parameters: None
Output: None
"""
def put_refresh_reachability(self):
uri = '/myplex/refreshReachability'
request = self.request_handler.make_request(uri=uri,
proto=self.protocol,
request_type='PUT')
return request
"""
Return processed and validated list of recently added items.
Parameters required: count { number of results to return }
@@ -281,10 +328,13 @@ class PmsConnect(object):
recents_main = a.getElementsByTagName('Directory')
for item in recents_main:
recent_type = helpers.get_xml_attr(item, 'type')
recent_items = {'type': recent_type,
recent_items = {'media_type': recent_type,
'rating_key': helpers.get_xml_attr(item, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(item, 'parentRatingKey'),
'title': helpers.get_xml_attr(item, 'title'),
'parent_title': helpers.get_xml_attr(item, 'parentTitle'),
'library_id': helpers.get_xml_attr(item, 'librarySectionID'),
'library_title': helpers.get_xml_attr(item, 'librarySectionTitle'),
'thumb': helpers.get_xml_attr(item, 'thumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt')
}
@@ -296,10 +346,12 @@ class PmsConnect(object):
recent_type = helpers.get_xml_attr(item, 'type')
if recent_type == 'movie':
recent_items = {'type': recent_type,
recent_items = {'media_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'),
'library_id': helpers.get_xml_attr(item, 'librarySectionID'),
'library_title': helpers.get_xml_attr(item, 'librarySectionTitle'),
'year': helpers.get_xml_attr(item, 'year'),
'thumb': helpers.get_xml_attr(item, 'thumb'),
'added_at': helpers.get_xml_attr(item, 'addedAt')
@@ -369,7 +421,7 @@ class PmsConnect(object):
directors.append(helpers.get_xml_attr(director, 'tag'))
if metadata_type == 'show':
metadata = {'type': metadata_type,
metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -401,16 +453,16 @@ class PmsConnect(object):
elif metadata_type == 'season':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
show_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type,
metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'parent_rating_key': helpers.get_xml_attr(metadata_main, 'parentRatingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'index': helpers.get_xml_attr(metadata_main, 'index'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
'studio': show_details['metadata']['studio'],
'title': helpers.get_xml_attr(metadata_main, 'title'),
'content_rating': helpers.get_xml_attr(metadata_main, 'contentRating'),
'content_rating': show_details['metadata']['content_rating'],
'summary': show_details['metadata']['summary'],
'tagline': helpers.get_xml_attr(metadata_main, 'tagline'),
'rating': helpers.get_xml_attr(metadata_main, 'rating'),
@@ -425,14 +477,16 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': genres,
'actors': actors,
'writers': writers,
'directors': directors
'genres': show_details['metadata']['genres'],
'actors': show_details['metadata']['actors'],
'writers': show_details['metadata']['writers'],
'directors': show_details['metadata']['directors']
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'episode':
metadata = {'type': metadata_type,
grandparent_rating_key = helpers.get_xml_attr(metadata_main, 'grandparentRatingKey')
show_details = self.get_metadata_details(grandparent_rating_key)
metadata = {'media_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'),
@@ -440,7 +494,7 @@ class PmsConnect(object):
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
'parent_title': helpers.get_xml_attr(metadata_main, 'parentTitle'),
'index': helpers.get_xml_attr(metadata_main, 'index'),
'studio': helpers.get_xml_attr(metadata_main, 'studio'),
'studio': show_details['metadata']['studio'],
'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'),
@@ -457,14 +511,14 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': show_details['metadata']['genres'],
'actors': show_details['metadata']['actors'],
'writers': writers,
'directors': directors,
'genres': genres,
'actors': actors
'directors': directors
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'movie':
metadata = {'type': metadata_type,
metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -494,7 +548,7 @@ class PmsConnect(object):
}
metadata_list = {'metadata': metadata}
elif metadata_type == 'artist':
metadata = {'type': metadata_type,
metadata = {'media_type': metadata_type,
'rating_key': helpers.get_xml_attr(metadata_main, 'ratingKey'),
'grandparent_title': helpers.get_xml_attr(metadata_main, 'grandparentTitle'),
'parent_index': helpers.get_xml_attr(metadata_main, 'parentIndex'),
@@ -526,7 +580,7 @@ class PmsConnect(object):
elif metadata_type == 'album':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
artist_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type,
metadata = {'media_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'),
@@ -559,7 +613,7 @@ class PmsConnect(object):
elif metadata_type == 'track':
parent_rating_key = helpers.get_xml_attr(metadata_main, 'parentRatingKey')
album_details = self.get_metadata_details(parent_rating_key)
metadata = {'type': metadata_type,
metadata = {'media_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'),
@@ -584,7 +638,7 @@ class PmsConnect(object):
'updated_at': helpers.get_xml_attr(metadata_main, 'updatedAt'),
'last_viewed_at': helpers.get_xml_attr(metadata_main, 'lastViewedAt'),
'guid': helpers.get_xml_attr(metadata_main, 'guid'),
'genres': genres,
'genres': album_details['metadata']['genres'],
'actors': actors,
'writers': writers,
'directors': directors
@@ -595,6 +649,49 @@ class PmsConnect(object):
return metadata_list
"""
Return processed and validated metadata list for all children of requested item.
Parameters required: rating_key { Plex ratingKey }
Output: array
"""
def get_metadata_children_details(self, rating_key=''):
metadata = self.get_metadata_children(str(rating_key), output_format='xml')
try:
xml_head = metadata.getElementsByTagName('MediaContainer')
except:
logger.warn("Unable to parse XML for get_metadata_children.")
return []
metadata_list = []
for a in xml_head:
if a.getAttribute('size'):
if a.getAttribute('size') == '0':
metadata_list = {'metadata': None}
return metadata_list
if a.getElementsByTagName('Video'):
metadata_main = a.getElementsByTagName('Video')
for item in metadata_main:
child_rating_key = helpers.get_xml_attr(item, 'ratingKey')
metadata = self.get_metadata_details(str(child_rating_key))
if metadata:
metadata_list.append(metadata['metadata'])
elif a.getElementsByTagName('Track'):
metadata_main = a.getElementsByTagName('Track')
for item in metadata_main:
child_rating_key = helpers.get_xml_attr(item, 'ratingKey')
metadata = self.get_metadata_details(str(child_rating_key))
if metadata:
metadata_list.append(metadata['metadata'])
output = {'metadata': metadata_list}
return output
"""
Return processed and validated session list.
@@ -706,6 +803,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -717,7 +815,7 @@ class PmsConnect(object):
'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_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
@@ -826,6 +924,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -838,7 +937,7 @@ class PmsConnect(object):
'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_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
@@ -882,6 +981,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -894,7 +994,7 @@ class PmsConnect(object):
'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_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
@@ -938,6 +1038,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -950,7 +1051,7 @@ class PmsConnect(object):
'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_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': audio_decision,
'audio_channels': audio_channels,
@@ -1027,6 +1128,7 @@ class PmsConnect(object):
'user_id': user_details['user_id'],
'friendly_name': user_details['friendly_name'],
'user_thumb': user_details['thumb'],
'ip_address': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'address').split(':')[-1],
'player': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'title'),
'platform': helpers.get_xml_attr(session.getElementsByTagName('Player')[0], 'platform'),
'machine_id': machine_id,
@@ -1039,7 +1141,7 @@ class PmsConnect(object):
'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_progress': int(round(helpers.cast_to_float(transcode_progress), 0)),
'transcode_speed': str(round(helpers.cast_to_float(transcode_speed), 1)),
'audio_decision': '',
'audio_channels': '',
@@ -1071,11 +1173,8 @@ class PmsConnect(object):
logger.warn(u"No known stream types found in session list.")
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
session_output['platform'] = platform_names.get(session_output['platform'],
session_output['platform'])
session_output['platform'] = common.PLATFORM_NAME_OVERRIDES.get(session_output['platform'],
session_output['platform'])
return session_output
@@ -1432,9 +1531,9 @@ class PmsConnect(object):
for result in result_data:
rating_key = helpers.get_xml_attr(result, 'ratingKey')
metadata = self.get_metadata_details(rating_key=rating_key)
if metadata['metadata']['type'] == 'movie':
if metadata['metadata']['media_type'] == 'movie':
search_results_list['movie'].append(metadata['metadata'])
elif metadata['metadata']['type'] == 'episode':
elif metadata['metadata']['media_type'] == 'episode':
search_results_list['episode'].append(metadata['metadata'])
search_results_count += 1
@@ -1443,7 +1542,7 @@ class PmsConnect(object):
for result in result_data:
rating_key = helpers.get_xml_attr(result, 'ratingKey')
metadata = self.get_metadata_details(rating_key=rating_key)
if metadata['metadata']['type'] == 'show':
if metadata['metadata']['media_type'] == 'show':
search_results_list['show'].append(metadata['metadata'])
show_seasons = self.get_item_children(rating_key=metadata['metadata']['rating_key'])
@@ -1455,9 +1554,9 @@ class PmsConnect(object):
search_results_list['season'].append(metadata['metadata'])
search_results_count += 1
elif metadata['metadata']['type'] == 'artist':
elif metadata['metadata']['media_type'] == 'artist':
search_results_list['artist'].append(metadata['metadata'])
elif metadata['metadata']['type'] == 'album':
elif metadata['metadata']['media_type'] == 'album':
search_results_list['album'].append(metadata['metadata'])
search_results_count += 1
@@ -1579,4 +1678,26 @@ class PmsConnect(object):
'children': parents}
}
return key_list
return key_list
def get_server_response(self):
# Refresh Plex remote access port mapping first
self.put_refresh_reachability()
account_data = self.get_account(output_format='xml')
try:
xml_head = account_data.getElementsByTagName('MyPlex')
except:
logger.warn("Unable to parse XML for get_server_response.")
return None
server_response = {}
for a in xml_head:
server_response = {'mapping_state': helpers.get_xml_attr(a, 'mappingState'),
'mapping_error': helpers.get_xml_attr(a, 'mappingError'),
'public_address': helpers.get_xml_attr(a, 'publicAddress'),
'public_port': helpers.get_xml_attr(a, 'publicPort')
}
return server_response

View File

@@ -24,6 +24,8 @@ class Users(object):
def get_user_list(self, kwargs=None):
data_tables = datatables.DataTables()
custom_where = ['users.deleted_user', 0]
columns = ['session_history.id',
'users.user_id as user_id',
'users.custom_avatar_url as user_thumb',
@@ -48,7 +50,7 @@ class Users(object):
try:
query = data_tables.ssp_query(table_name='users',
columns=columns,
custom_where=[],
custom_where=[custom_where],
group_by=['users.user_id'],
join_types=['LEFT OUTER JOIN',
'LEFT OUTER JOIN',
@@ -85,10 +87,7 @@ class Users(object):
user_thumb = item['user_thumb']
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
row = {"id": item['id'],
"plays": item['plays'],
@@ -96,7 +95,7 @@ class Users(object):
"friendly_name": item['friendly_name'],
"ip_address": item['ip_address'],
"platform": platform,
"player": item['player'],
"player": item["player"],
"last_watched": item['last_watched'],
"thumb": thumb,
"media_type": item['media_type'],
@@ -179,10 +178,7 @@ class Users(object):
thumb = item["thumb"]
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform = platform_names.get(item["platform"], item["platform"])
platform = common.PLATFORM_NAME_OVERRIDES.get(item["platform"], item["platform"])
row = {"id": item['id'],
"last_seen": item['last_seen'],
@@ -269,17 +265,17 @@ class Users(object):
if user_id:
monitor_db = database.MonitorDatabase()
query = 'select username, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) as friendly_name,' \
'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])
'user': result[0]['username'],
'friendly_name': result[0]['friendly_name'],
'thumb': result[0]['thumb'],
'do_notify': helpers.checked(result[0]['do_notify']),
'keep_history': helpers.checked(result[0]['keep_history'])
}
return user_detail
else:
@@ -293,17 +289,17 @@ class Users(object):
elif user:
monitor_db = database.MonitorDatabase()
query = 'select user_id, ' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END),' \
'(CASE WHEN friendly_name IS NULL THEN username ELSE friendly_name END) as friendly_name,' \
'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_detail = {'user_id': result[0]['user_id'],
'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])}
'friendly_name': result[0]['friendly_name'],
'thumb': result[0]['thumb'],
'do_notify': helpers.checked(result[0]['do_notify']),
'keep_history': helpers.checked(result[0]['keep_history'])}
return user_detail
else:
user_detail = {'user_id': None,
@@ -323,7 +319,7 @@ class Users(object):
query = 'select user_id FROM users WHERE username = ?'
result = monitor_db.select_single(query, args=[user])
if result:
return result
return result['user_id']
else:
return None
except:
@@ -490,9 +486,9 @@ class Users(object):
result = monitor_db.select(query, args=[user])
for item in result:
if item[0]:
total_time = item[0]
total_plays = item[1]
if item['total_time']:
total_time = item['total_time']
total_plays = item['total_plays']
else:
total_time = 0
total_plays = 0
@@ -533,17 +529,14 @@ class Users(object):
for item in result:
# Rename Mystery platform names
platform_names = {'Mystery 3': 'Playstation 3',
'Mystery 4': 'Playstation 4',
'Mystery 5': 'Xbox 360'}
platform_type = platform_names.get(item[2], item[2])
platform_type = common.PLATFORM_NAME_OVERRIDES.get(item['platform'], item['platform'])
row = {'player_name': item[0],
row = {'player_name': item['player'],
'platform_type': platform_type,
'total_plays': item[1],
'total_plays': item['player_count'],
'result_id': result_id
}
player_stats.append(row)
result_id += 1
return player_stats
return player_stats

View File

@@ -1,2 +1,2 @@
PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.2.3"
PLEXPY_VERSION = "master"
PLEXPY_RELEASE_VERSION = "1.2.15"

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
@@ -139,4 +139,14 @@ def process(opcode, data):
activity = activity_handler.ActivityHandler(timeline=time_line[0])
activity.process()
#if type == 'timeline':
# try:
# time_line = info.get('_children')
# except:
# logger.debug(u"PlexPy WebSocket :: Timeline event found but unable to get timeline data.")
# return False
# activity = activity_handler.TimelineHandler(timeline=time_line[0])
# activity.process()
return True

View File

@@ -1,7 +1,4 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# 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
@@ -16,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, users
from plexpy import logger, notifiers, plextv, pmsconnect, common, log_reader, datafactory, graphs, users, helpers
from plexpy.helpers import checked, radio
from mako.lookup import TemplateLookup
@@ -44,11 +41,13 @@ def serve_template(templatename, **kwargs):
interface_dir = os.path.join(str(plexpy.PROG_DIR), 'data/interfaces/')
template_dir = os.path.join(str(interface_dir), plexpy.CONFIG.INTERFACE)
_hplookup = TemplateLookup(directories=[template_dir])
_hplookup = TemplateLookup(directories=[template_dir], default_filters=['unicode', 'h'])
server_name = plexpy.CONFIG.PMS_NAME
try:
template = _hplookup.get_template(templatename)
return template.render(**kwargs)
return template.render(server_name=server_name, **kwargs)
except:
return exceptions.html_error_template().render()
@@ -71,7 +70,8 @@ class WebInterface(object):
"home_stats_length": plexpy.CONFIG.HOME_STATS_LENGTH,
"home_stats_cards": plexpy.CONFIG.HOME_STATS_CARDS,
"home_library_cards": plexpy.CONFIG.HOME_LIBRARY_CARDS,
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER
"pms_identifier": plexpy.CONFIG.PMS_IDENTIFIER,
"pms_name": plexpy.CONFIG.PMS_NAME
}
return serve_template(templatename="index.html", title="Home", config=config)
@@ -87,13 +87,14 @@ class WebInterface(object):
"pms_token": plexpy.CONFIG.PMS_TOKEN,
"pms_ssl": checked(plexpy.CONFIG.PMS_SSL),
"pms_uuid": plexpy.CONFIG.PMS_UUID,
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
"music_notify_on_start": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_START),
"video_logging_enable": checked(plexpy.CONFIG.VIDEO_LOGGING_ENABLE),
"movie_logging_enable": checked(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
"tv_logging_enable": checked(plexpy.CONFIG.TV_LOGGING_ENABLE),
"music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"check_github": checked(plexpy.CONFIG.CHECK_GITHUB)
@@ -127,13 +128,15 @@ class WebInterface(object):
def home_stats(self, **kwargs):
data_factory = datafactory.DataFactory()
grouping = plexpy.CONFIG.GROUP_HISTORY_TABLES
time_range = plexpy.CONFIG.HOME_STATS_LENGTH
stats_type = plexpy.CONFIG.HOME_STATS_TYPE
stats_count = plexpy.CONFIG.HOME_STATS_COUNT
stats_cards = plexpy.CONFIG.HOME_STATS_CARDS.split(', ')
notify_watched_percent = plexpy.CONFIG.NOTIFY_WATCHED_PERCENT
stats_data = data_factory.get_home_stats(time_range=time_range,
stats_data = data_factory.get_home_stats(grouping=grouping,
time_range=time_range,
stats_type=stats_type,
stats_count=stats_count,
stats_cards=stats_cards,
@@ -174,6 +177,9 @@ class WebInterface(object):
def graphs(self):
config = {
"graph_type": plexpy.CONFIG.GRAPH_TYPE,
"graph_days": plexpy.CONFIG.GRAPH_DAYS,
"graph_tab": plexpy.CONFIG.GRAPH_TAB,
"music_logging_enable": plexpy.CONFIG.MUSIC_LOGGING_ENABLE
}
@@ -420,8 +426,8 @@ class WebInterface(object):
"grouping_global_history": checked(plexpy.CONFIG.GROUPING_GLOBAL_HISTORY),
"grouping_user_history": checked(plexpy.CONFIG.GROUPING_USER_HISTORY),
"grouping_charts": checked(plexpy.CONFIG.GROUPING_CHARTS),
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"movie_notify_enable": checked(plexpy.CONFIG.MOVIE_NOTIFY_ENABLE),
"tv_notify_enable": checked(plexpy.CONFIG.TV_NOTIFY_ENABLE),
"music_notify_enable": checked(plexpy.CONFIG.MUSIC_NOTIFY_ENABLE),
"tv_notify_on_start": checked(plexpy.CONFIG.TV_NOTIFY_ON_START),
"movie_notify_on_start": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_START),
@@ -432,16 +438,21 @@ class WebInterface(object):
"tv_notify_on_pause": checked(plexpy.CONFIG.TV_NOTIFY_ON_PAUSE),
"movie_notify_on_pause": checked(plexpy.CONFIG.MOVIE_NOTIFY_ON_PAUSE),
"music_notify_on_pause": checked(plexpy.CONFIG.MUSIC_NOTIFY_ON_PAUSE),
"monitor_remote_access": checked(plexpy.CONFIG.MONITOR_REMOTE_ACCESS),
"monitoring_interval": plexpy.CONFIG.MONITORING_INTERVAL,
"monitoring_use_websocket": checked(plexpy.CONFIG.MONITORING_USE_WEBSOCKET),
"refresh_users_interval": plexpy.CONFIG.REFRESH_USERS_INTERVAL,
"refresh_users_on_startup": checked(plexpy.CONFIG.REFRESH_USERS_ON_STARTUP),
"ip_logging_enable": checked(plexpy.CONFIG.IP_LOGGING_ENABLE),
"video_logging_enable": checked(plexpy.CONFIG.VIDEO_LOGGING_ENABLE),
"movie_logging_enable": checked(plexpy.CONFIG.MOVIE_LOGGING_ENABLE),
"tv_logging_enable": checked(plexpy.CONFIG.TV_LOGGING_ENABLE),
"music_logging_enable": checked(plexpy.CONFIG.MUSIC_LOGGING_ENABLE),
"logging_ignore_interval": plexpy.CONFIG.LOGGING_IGNORE_INTERVAL,
"pms_is_remote": checked(plexpy.CONFIG.PMS_IS_REMOTE),
"notify_consecutive": checked(plexpy.CONFIG.NOTIFY_CONSECUTIVE),
"notify_recently_added": checked(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED),
"notify_recently_added_grandparent": checked(plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_GRANDPARENT),
"notify_recently_added_delay": plexpy.CONFIG.NOTIFY_RECENTLY_ADDED_DELAY,
"notify_watched_percent": plexpy.CONFIG.NOTIFY_WATCHED_PERCENT,
"notify_on_start_subject_text": plexpy.CONFIG.NOTIFY_ON_START_SUBJECT_TEXT,
"notify_on_start_body_text": plexpy.CONFIG.NOTIFY_ON_START_BODY_TEXT,
@@ -455,6 +466,16 @@ class WebInterface(object):
"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_created_subject_text": plexpy.CONFIG.NOTIFY_ON_CREATED_SUBJECT_TEXT,
"notify_on_created_body_text": plexpy.CONFIG.NOTIFY_ON_CREATED_BODY_TEXT,
"notify_on_extdown_subject_text": plexpy.CONFIG.NOTIFY_ON_EXTDOWN_SUBJECT_TEXT,
"notify_on_extdown_body_text": plexpy.CONFIG.NOTIFY_ON_EXTDOWN_BODY_TEXT,
"notify_on_intdown_subject_text": plexpy.CONFIG.NOTIFY_ON_INTDOWN_SUBJECT_TEXT,
"notify_on_intdown_body_text": plexpy.CONFIG.NOTIFY_ON_INTDOWN_BODY_TEXT,
"notify_on_extup_subject_text": plexpy.CONFIG.NOTIFY_ON_EXTUP_SUBJECT_TEXT,
"notify_on_extup_body_text": plexpy.CONFIG.NOTIFY_ON_EXTUP_BODY_TEXT,
"notify_on_intup_subject_text": plexpy.CONFIG.NOTIFY_ON_INTUP_SUBJECT_TEXT,
"notify_on_intup_body_text": plexpy.CONFIG.NOTIFY_ON_INTUP_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,
@@ -474,12 +495,13 @@ class WebInterface(object):
checked_configs = [
"launch_browser", "enable_https", "api_enabled", "freeze_db", "check_github",
"grouping_global_history", "grouping_user_history", "grouping_charts", "pms_use_bif", "pms_ssl",
"tv_notify_enable", "movie_notify_enable", "music_notify_enable", "monitoring_use_websocket",
"movie_notify_enable", "tv_notify_enable", "music_notify_enable", "monitoring_use_websocket",
"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", "home_stats_type",
"group_history_tables", "notify_consecutive"
"ip_logging_enable", "movie_logging_enable", "tv_logging_enable", "music_logging_enable",
"pms_is_remote", "home_stats_type", "group_history_tables", "notify_consecutive",
"notify_recently_added", "notify_recently_added_grandparent", "monitor_remote_access"
]
for checked_config in checked_configs:
if checked_config not in kwargs:
@@ -504,6 +526,14 @@ class WebInterface(object):
if (kwargs['monitoring_interval'] != str(plexpy.CONFIG.MONITORING_INTERVAL)) or \
(kwargs['refresh_users_interval'] != str(plexpy.CONFIG.REFRESH_USERS_INTERVAL)):
reschedule = True
if 'notify_recently_added' in kwargs and \
(kwargs['notify_recently_added'] != plexpy.CONFIG.NOTIFY_RECENTLY_ADDED):
reschedule = True
if 'monitor_remote_access' in kwargs and \
(kwargs['monitor_remote_access'] != plexpy.CONFIG.MONITOR_REMOTE_ACCESS):
reschedule = True
if 'pms_ip' in kwargs:
if kwargs['pms_ip'] != plexpy.CONFIG.PMS_IP:
@@ -571,27 +601,28 @@ class WebInterface(object):
custom_where = []
if user_id:
custom_where = [['session_history.user_id', user_id]]
custom_where.append(['session_history.user_id', user_id])
elif user:
custom_where = [['session_history.user', user]]
custom_where.append(['session_history.user', user])
if 'rating_key' in kwargs:
rating_key = kwargs.get('rating_key', "")
custom_where = [['session_history.rating_key', rating_key]]
custom_where.append(['session_history.rating_key', rating_key])
if 'parent_rating_key' in kwargs:
rating_key = kwargs.get('parent_rating_key', "")
custom_where = [['session_history.parent_rating_key', rating_key]]
custom_where.append(['session_history.parent_rating_key', rating_key])
if 'grandparent_rating_key' in kwargs:
rating_key = kwargs.get('grandparent_rating_key', "")
custom_where = [['session_history.grandparent_rating_key', rating_key]]
custom_where.append(['session_history.grandparent_rating_key', rating_key])
if 'start_date' in kwargs:
start_date = kwargs.get('start_date', "")
custom_where = [['strftime("%Y-%m-%d", datetime(date, "unixepoch", "localtime"))', start_date]]
custom_where.append(['strftime("%Y-%m-%d", datetime(started, "unixepoch", "localtime"))', start_date])
if 'reference_id' in kwargs:
reference_id = kwargs.get('reference_id', "")
custom_where = [['session_history.reference_id', reference_id]]
custom_where.append(['session_history.reference_id', reference_id])
if 'media_type' in kwargs:
media_type = kwargs.get('media_type', "")
custom_where = [['session_history_metadata.media_type', media_type]]
if media_type != 'all':
custom_where.append(['session_history.media_type', media_type])
data_factory = datafactory.DataFactory()
history = data_factory.get_history(kwargs=kwargs, custom_where=custom_where, grouping=grouping, watched_percent=watched_percent)
@@ -652,6 +683,16 @@ class WebInterface(object):
else:
return "Error sending tweet"
@cherrypy.expose
def test_ifttt(self):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
event = notifiers.IFTTT()
result = event.test()
if result:
return "Notification successful."
else:
return "Error sending event."
@cherrypy.expose
def osxnotifyregister(self, app):
cherrypy.response.headers['Cache-Control'] = "max-age=0,no-cache,no-store"
@@ -697,6 +738,13 @@ class WebInterface(object):
try:
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_current_activity()
data_factory = datafactory.DataFactory()
for session in result['sessions']:
if not session['ip_address']:
ip_address = data_factory.get_session_ip(session['session_key'])
session['ip_address'] = ip_address
except:
return serve_template(templatename="current_activity.html", data=None)
@@ -777,11 +825,11 @@ class WebInterface(object):
data_factory = datafactory.DataFactory()
metadata = data_factory.get_metadata_details(row_id=item_id)
elif item_id == 'movie':
metadata = {'type': 'library', 'library': 'movie', 'media_type': 'movie', 'title': 'Movies'}
metadata = {'media_type': 'library', 'library': 'movie', 'media_type_filter': 'movie', 'title': 'Movies'}
elif item_id == 'show':
metadata = {'type': 'library', 'library': 'show', 'media_type': 'episode', 'title': 'TV Shows'}
metadata = {'media_type': 'library', 'library': 'show', 'media_type_filter': 'episode', 'title': 'TV Shows'}
elif item_id == 'artist':
metadata = {'type': 'library', 'library': 'artist', 'media_type': 'track', 'title': 'Music'}
metadata = {'media_type': 'library', 'library': 'artist', 'media_type_filter': 'track', 'title': 'Music'}
else:
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_metadata_details(rating_key=item_id)
@@ -912,6 +960,20 @@ class WebInterface(object):
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps(history)
@cherrypy.expose
def set_graph_config(self, graph_type=None, graph_days=None, graph_tab=None):
if graph_type:
plexpy.CONFIG.__setattr__('GRAPH_TYPE', graph_type)
plexpy.CONFIG.write()
if graph_days:
plexpy.CONFIG.__setattr__('GRAPH_DAYS', graph_days)
plexpy.CONFIG.write()
if graph_tab:
plexpy.CONFIG.__setattr__('GRAPH_TAB', graph_tab)
plexpy.CONFIG.write()
return "Updated graphs config values."
@cherrypy.expose
def get_plays_by_date(self, time_range='30', y_axis='plays', **kwargs):
@@ -1117,10 +1179,24 @@ class WebInterface(object):
logger.warn('Unable to retrieve data.')
@cherrypy.expose
def get_server_prefs(self, **kwargs):
def get_server_friendly_name(self, **kwargs):
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_server_prefs(output_format='json')
result = pmsconnect.get_server_friendly_name()
if result:
cherrypy.response.headers['Content-type'] = 'application/json'
return result
else:
logger.warn('Unable to retrieve data.')
@cherrypy.expose
def get_server_prefs(self, pref=None, **kwargs):
if pref:
pms_connect = pmsconnect.PmsConnect()
result = pms_connect.get_server_pref(pref=pref)
else:
result = None
if result:
cherrypy.response.headers['Content-type'] = 'application/json'
@@ -1300,12 +1376,22 @@ class WebInterface(object):
@cherrypy.expose
def get_notification_agent_config(self, config_id, **kwargs):
config = notifiers.get_notification_agent_config(config_id=config_id)
if config_id.isdigit():
config = notifiers.get_notification_agent_config(config_id=config_id)
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
checkboxes = {'email_tls': checked(plexpy.CONFIG.EMAIL_TLS)}
return serve_template(templatename="notification_config.html", title="Notification Configuration",
config_id=config_id, data=config, checkboxes=checkboxes)
agent=this_agent, data=config, checkboxes=checkboxes)
@cherrypy.expose
def get_notification_agent_triggers(self, config_id, **kwargs):
@@ -1352,8 +1438,41 @@ class WebInterface(object):
return json.dumps({'message': 'no data received'})
@cherrypy.expose
def search(self, search_query=''):
query = search_query.replace('"', '')
def delete_user(self, user_id, **kwargs):
data_factory = datafactory.DataFactory()
if user_id:
delete_row = data_factory.delete_user(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'})
@cherrypy.expose
def undelete_user(self, user_id=None, username=None, **kwargs):
data_factory = datafactory.DataFactory()
if user_id:
delete_row = data_factory.undelete_user(user_id=user_id)
if delete_row:
cherrypy.response.headers['Content-type'] = 'application/json'
return json.dumps({'message': delete_row})
elif username:
delete_row = data_factory.undelete_user(username=username)
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 search(self, query=''):
return serve_template(templatename="search.html", title="Search", query=query)