Compare commits

...

532 Commits

Author SHA1 Message Date
JonnyWong16
b1dc5816a4 v2.1.4 2018-05-05 11:21:03 -07:00
JonnyWong16
476011a783 Fix newsletter URL with no HTTP root 2018-05-05 11:13:27 -07:00
JonnyWong16
e038c57c4c v2.1.3-beta 2018-05-04 22:36:11 -07:00
JonnyWong16
a989a53750 Encode image title for Cloudinary upload 2018-05-04 16:11:42 -07:00
JonnyWong16
d8cfdea704 Log individual condition evalutation 2018-05-04 15:52:04 -07:00
JonnyWong16
ed4722c4ce Improve refreshing of cached Plex images 2018-05-03 20:29:23 -07:00
JonnyWong16
17ab5f05ed Patch apshceduler sun-sat as 0-6 2018-05-03 17:58:28 -07:00
JonnyWong16
71ab2248d7 Make sure Cloudinary parameters are strings 2018-05-03 08:34:32 -07:00
JonnyWong16
4fb4410552 Fix potential XSS in search 2018-05-02 10:26:05 -07:00
JonnyWong16
a915d2333f Catch failed hostname resolution (Fixes Tautulli/Tautulli-Issues#68) 2018-05-01 16:57:43 -07:00
JonnyWong16
aaf5a18251 Forgot missing '/' 2018-05-01 15:51:23 -07:00
JonnyWong16
b90026801b Fix double HTTP root in newsletter URL 2018-05-01 15:37:37 -07:00
JonnyWong16
e9676e3651 v2.1.2-beta 2018-05-01 08:47:32 -07:00
JonnyWong16
c16d3288d8 Update Imgur and Cloudinary help text 2018-04-29 21:18:01 -07:00
JonnyWong16
0d7ade8ca4 Transform images on Cloudinary 2018-04-29 18:46:46 -07:00
JonnyWong16
87b1118e98 Add delete from Cloudinary 2018-04-29 17:49:53 -07:00
JonnyWong16
9f6422cc8d Fix Imgur poster lookup on imfo pages 2018-04-29 16:04:46 -07:00
JonnyWong16
df1a42a4ee Fix update metadata z-index 2018-04-29 15:15:57 -07:00
JonnyWong16
6554136a8f Add Cloudinary image hosting option 2018-04-29 00:11:47 -07:00
JonnyWong16
81e04269fd Remove ratelimit library 2018-04-29 00:10:58 -07:00
JonnyWong16
b6c6590a12 Update six v1.11.0 2018-04-28 21:44:34 -07:00
JonnyWong16
136260a822 Add cloudinary v1.11.0 2018-04-28 21:44:19 -07:00
JonnyWong16
5710bcb43c Hardcode Pushover sounds list (Fixes Tautulli/Tautulli-Issues#65) 2018-04-28 20:07:36 -07:00
JonnyWong16
30bc3f8a66 Fix incorrect {action} for new device (Fixes Tautulli/Tautulli-Issues#63) 2018-04-28 19:59:23 -07:00
JonnyWong16
e0e7d68df2 API success result for empty response data (Fixes Tautulli/Tautulli-Issues#56) 2018-04-28 18:39:42 -07:00
JonnyWong16
cf73639281 Fix Twitter notification with self-hosted images (Fixes Tautulli/Tautulli-Issues#54) 2018-04-28 18:23:10 -07:00
JonnyWong16
008e04d5cf Re-factor script timeout code 2018-04-28 18:12:43 -07:00
JonnyWong16
5f7991665c Only notify Tautulli updates when checked as a scheduled task (Fixes Tautulli/Tautulli-Issues#46) 2018-04-28 17:59:53 -07:00
JonnyWong16
5e000162c6 Merge branch 'nightly' of https://github.com/Tautulli/Tautulli into nightly 2018-04-28 17:54:18 -07:00
JonnyWong16
ea1aba2c87 Merge pull request #1286 from Dam64/patch-1
add Message-ID on mails
2018-04-28 17:54:02 -07:00
JonnyWong16
f321bb869c Add Plex Cloud sleeping message 2018-04-28 17:51:52 -07:00
JonnyWong16
abe496668a Fix typo in Activity Refresh Interval setting 2018-04-28 17:48:55 -07:00
Dam64
9cefc7f701 add Message-ID on mails 2018-04-27 01:17:18 +02:00
JonnyWong16
1d3cd431eb v2.1.1-beta 2018-04-11 21:59:39 -07:00
JonnyWong16
8f8318da6d Fix Imgur fallback to cover on newsletters 2018-04-11 21:42:14 -07:00
JonnyWong16
36ce751875 Explicit white font colour on newsletter cards 2018-04-11 12:04:00 -07:00
JonnyWong16
858ea33680 Fix fallback to cover for albums on newsletter 2018-04-11 11:58:57 -07:00
JonnyWong16
eee759d0d0 Log newsletter start time and end time in database 2018-04-10 22:44:11 -07:00
JonnyWong16
dbe3b492fd Check if the newsletter has data before saving the html file 2018-04-10 22:28:48 -07:00
JonnyWong16
4e4fde2e9a Move time frame to global newsletter configs 2018-04-10 21:34:18 -07:00
JonnyWong16
5283126608 Week number of the year 2018-04-10 21:31:25 -07:00
JonnyWong16
df72ecebf5 Add hours as time frame for newsletters 2018-04-10 21:16:16 -07:00
JonnyWong16
d316aa34e2 Merge pull request #1283 from samip5/week_number-patch
Adding the week number parameter
2018-04-10 20:17:07 -07:00
JonnyWong16
405aec8bb8 User helper for casting condition values 2018-04-10 19:57:40 -07:00
samip5
4a62f8c395 Fixed a typo. 2018-04-10 20:15:26 +03:00
samip5
eabea2deeb Made the requested changes.
The requested changes by JonnyWong16 in the PR request were done in this
commit.
2018-04-10 20:07:56 +03:00
samip5
3742021dcc Removed the non-needed imports. 2018-04-10 13:20:30 +03:00
samip5
9c4219b42e Edited newsletters.py
It wouldn't want to work without the edit.
2018-04-10 13:19:29 +03:00
samip5
f624908302 Added a new branch, edited the code to include the week_number 2018-04-10 12:10:32 +03:00
samip5
ab9132cdd4 Made sure the syntax is understandable. 2018-04-09 17:27:13 +03:00
samip5
0186363753 Added the week number parameter. 2018-04-09 17:20:51 +03:00
JonnyWong16
653ad36f17 Move transcode decision after live session override 2018-04-08 15:14:35 -07:00
JonnyWong16
5073f82d53 Another fix for Live TV stream transcode decision 2018-04-08 15:09:23 -07:00
JonnyWong16
833937eced Fix Live TV transcode details (Fixes Tautulli/Tautulli-Issues#45) 2018-04-08 11:16:43 -07:00
JonnyWong16
32df79bb83 Only sanitize script output when viewing the logs in the UI 2018-04-08 10:44:47 -07:00
JonnyWong16
fabced9942 Add tqdm v4.21.0 2018-04-08 10:44:04 -07:00
JonnyWong16
8aa34321c9 Add plexapi v3.0.6 2018-04-08 10:43:33 -07:00
JonnyWong16
b144ded87b v2.1.0-beta 2018-04-07 15:23:03 -07:00
JonnyWong16
ef8c91ee56 Fix new email config showing when HTML formatted email is unchecked 2018-04-07 15:22:29 -07:00
JonnyWong16
d76ded3ebe Remove "Notify on" from notification trigger text 2018-04-07 15:13:20 -07:00
JonnyWong16
c4fc94ea34 Fix unicode log errors 2018-04-07 10:35:28 -07:00
JonnyWong16
ad61e23d92 Don't fallback to art on newsletters 2018-04-05 19:35:37 -07:00
JonnyWong16
fcd7593764 Add temporary watched state for sessions 2018-04-04 22:05:30 -07:00
JonnyWong16
8465df5095 Replace Imgur with Tautulli hosted newsletter assets 2018-04-04 21:37:50 -07:00
JonnyWong16
95697a3367 Fix homepage max 50 recently added items 2018-04-04 21:37:14 -07:00
JonnyWong16
978ae7d8cb Note that HTML support must be enabled for eamil 2018-04-04 21:01:29 -07:00
JonnyWong16
366e8514b6 Fix centering last item on newsletter 2018-04-04 20:45:34 -07:00
JonnyWong16
45c646c062 Fix test newsletter warning message 2018-04-02 15:27:27 -07:00
JonnyWong16
4b482938a1 Make sure simple cron is set when saving 2018-04-02 15:01:07 -07:00
JonnyWong16
9699129a38 Catch invalid cron entry and reset to default 2018-04-02 14:50:01 -07:00
JonnyWong16
5ef8947532 Merge v2.0.28 into v2-newsletters 2018-04-02 14:33:52 -07:00
JonnyWong16
f335ffa8d5 v2.0.28 2018-04-02 14:27:34 -07:00
JonnyWong16
793665d62a Revert home activity header 2018-04-02 14:25:54 -07:00
JonnyWong16
7da5730c73 Fix activity header text 2018-04-02 14:21:34 -07:00
JonnyWong16
1f587ed698 v2.0.27 2018-04-02 14:17:48 -07:00
JonnyWong16
1032fdfe7a Move refresh interval setting back to the settings page 2018-04-02 14:13:52 -07:00
JonnyWong16
35e3f7dccc Change crontab anchor text 2018-04-02 14:10:52 -07:00
JonnyWong16
909cbc90df Add toggle for simple/custom crontab 2018-04-02 13:10:29 -07:00
JonnyWong16
77ed94bbef Separate newsletter message and body text 2018-04-02 11:12:37 -07:00
JonnyWong16
c260543586 Add custom cron to newsletter schedule 2018-04-02 10:17:51 -07:00
JonnyWong16
a4de63095f Fix View on Plex overlay 2018-03-30 17:45:50 -07:00
JonnyWong16
817335b42e Separate values in img hash 2018-03-30 13:44:36 -07:00
JonnyWong16
818e7723ff v2.0.26-beta 2018-03-30 09:29:47 -07:00
JonnyWong16
a69008e179 Send Telegram notification separately if caption is longer than 200 characters (Closes Tautulli/Tautulli-Issues#20) 2018-03-30 09:23:38 -07:00
JonnyWong16
91c647f9ae Show extra type on activity cards 2018-03-29 19:54:43 -07:00
JonnyWong16
36b80aa6d3 Make sure all datatables are using POST 2018-03-28 18:08:57 -07:00
JonnyWong16
c35fcc727c Change default refresh to 10 seconds 2018-03-27 22:08:12 -07:00
JonnyWong16
749e1fcebe Move refresh interval setting to homepage 2018-03-26 08:53:40 -07:00
JonnyWong16
80506b8541 Add login redirect uri 2018-03-25 14:26:47 -07:00
JonnyWong16
80df2b0fad Add Imgur rate limiting 2018-03-25 13:47:49 -07:00
JonnyWong16
084732706d Add setting to change homepage refresh interval 2018-03-25 13:25:18 -07:00
JonnyWong16
2aff7713cd Fix invalid link to playlist in sync table (Fixes Tautulli/Tautulli-Issues#34) 2018-03-25 12:39:20 -07:00
JonnyWong16
683a782723 Fix typo (Closes Tautulli/Tautulli-Issues#35) 2018-03-25 11:58:57 -07:00
JonnyWong16
5108e1bb09 Add quick websocket test when verifying server 2018-03-25 11:38:35 -07:00
JonnyWong16
d8298a12eb Clear PMS selectize when dropdown opens 2018-03-25 11:00:58 -07:00
JonnyWong16
dec5931fd4 Fix some typos 2018-03-25 10:12:03 -07:00
JonnyWong16
71d79266f6 Offload homepage image processing to the Plex server 2018-03-25 00:31:04 -07:00
JonnyWong16
d3f6812178 Merge branch 'nightly' into v2-newsletter 2018-03-24 23:35:21 -07:00
JonnyWong16
042b48c1fd Fix repeating renaming notifiers on startup 2018-03-24 23:32:53 -07:00
JonnyWong16
38613f24fe Scroll to setting position 2018-03-24 23:09:30 -07:00
JonnyWong16
e23b1a0603 Add self-hosted notificaation images 2018-03-24 22:09:37 -07:00
JonnyWong16
90f3d597dc Reorganize all Imgur info in database 2018-03-24 20:02:23 -07:00
JonnyWong16
d166b77ea9 Link to season or episode 2018-03-24 18:09:36 -07:00
JonnyWong16
feb74b157f Move custom message into the body of the newsletter 2018-03-24 14:53:21 -07:00
JonnyWong16
4aeafdae2d Add custom body line to formatted email 2018-03-24 14:34:47 -07:00
JonnyWong16
f12de78370 Add warning about Imgur or self-hosting enabled 2018-03-24 13:46:15 -07:00
JonnyWong16
d2415c92ea Add option to send to a newsletter message to other notification agents 2018-03-24 13:45:59 -07:00
JonnyWong16
646ca1d9fa Add genre badges 2018-03-24 11:58:06 -07:00
JonnyWong16
c8c93c69ab Align text left on newsletter cards for Outlook 2018-03-24 11:17:50 -07:00
JonnyWong16
2c8c20af02 Add link to view full newsletter 2018-03-24 10:30:07 -07:00
JonnyWong16
a877da3de8 Fix newsletter header image sizes 2018-03-24 09:45:27 -07:00
JonnyWong16
1b7cfd7f8a Fix helper import 2018-03-24 09:05:20 -07:00
JonnyWong16
3f7edc3635 Redo newsletter template using tables 2018-03-24 00:02:31 -07:00
JonnyWong16
8fac54aa71 Typo 2018-03-22 22:11:11 -07:00
JonnyWong16
244008d539 v2.0.25 2018-03-22 22:06:01 -07:00
JonnyWong16
502b807e45 Fix websocket not scheduling reconnect 2018-03-22 21:03:11 -07:00
JonnyWong16
35914b9a48 Remove unicode from websocket logger error 2018-03-22 20:32:37 -07:00
JonnyWong16
24ac34d5e2 Make sure user has Plex Pass if checking for synced stream 2018-03-22 19:39:46 -07:00
JonnyWong16
e1035a49fd Move newsletter loader css to main file 2018-03-21 19:57:55 -07:00
JonnyWong16
511f4a916b Automatically append HTTP root to newsletter URL 2018-03-21 19:18:49 -07:00
JonnyWong16
1f10668838 Allow additional parameters for newsletter preview 2018-03-21 13:05:46 -07:00
JonnyWong16
a9a08a959c Don't return error messages if missing newsletter files 2018-03-21 12:14:18 -07:00
JonnyWong16
341f4040ff Hash image with UUID 2018-03-21 08:23:05 -07:00
JonnyWong16
e9a1b2ea38 Add self-hosted static images 2018-03-21 00:10:39 -07:00
JonnyWong16
7f67213ff7 Store image hash for self-hosted newsletters 2018-03-20 22:54:36 -07:00
JonnyWong16
e9bdbb863c Make self-hosted option global 2018-03-20 11:24:12 -07:00
JonnyWong16
04641c7c63 Encode UTF-8 when saving newsletter 2018-03-20 10:51:33 -07:00
JonnyWong16
15cc96a005 As self-hosted config option 2018-03-20 10:39:33 -07:00
JonnyWong16
b712874ed2 Fix email config for newsletters 2018-03-20 10:07:06 -07:00
JonnyWong16
5b1ff402bc Queue notification instead of waiting for send 2018-03-20 09:15:41 -07:00
JonnyWong16
eda0e73eb6 Queue newsletters instead of waiting for send 2018-03-20 09:09:49 -07:00
JonnyWong16
a5807f21b4 Flush temporary sessions automatically if failed to check sessions on startup 2018-03-19 23:24:09 -07:00
JonnyWong16
e3b71a729e Revert negative operator values to "OR" (UI change only) 2018-03-19 23:18:27 -07:00
JonnyWong16
f810f50ea9 Adjust newsletter log table widths 2018-03-19 23:10:25 -07:00
JonnyWong16
2b0f83e036 Newsletter body text for test 2018-03-19 23:07:19 -07:00
JonnyWong16
4977b3def1 Remove trailing slash 2018-03-19 23:04:09 -07:00
JonnyWong16
1cb5f0b635 Add newsletter base URL setting 2018-03-19 22:57:38 -07:00
JonnyWong16
7e11af1fd0 Add body text to newsletters 2018-03-19 20:20:57 -07:00
JonnyWong16
6f6fb485fe Log message no libraries selected for newsletter 2018-03-19 09:21:40 -07:00
JonnyWong16
964f24d6ab Fix music background on newsletter 2018-03-19 09:15:06 -07:00
JonnyWong16
1474f144fe View sent newsletters without authentication 2018-03-18 23:01:23 -07:00
JonnyWong16
8d25b0c973 Open Sans for newsletters 2018-03-18 21:56:44 -07:00
JonnyWong16
50b37d6b3a Add newsletter logs table 2018-03-18 21:40:57 -07:00
JonnyWong16
b9b82b23f7 Save newsletters to html file 2018-03-18 21:02:39 -07:00
JonnyWong16
b6bd305694 Merge branch 'nightly' into v2-newsletter 2018-03-18 17:49:14 -07:00
JonnyWong16
ebb287e1ee v2.0.24 2018-03-18 17:46:17 -07:00
JonnyWong16
bd3497b2bf Rename notifiers in database 2018-03-18 17:44:24 -07:00
JonnyWong16
034f3ee308 Anon URL to FAQ for pycryptodome does not work with anchors 2018-03-18 17:33:02 -07:00
JonnyWong16
a946879fc1 Better OSX register button 2018-03-18 17:22:44 -07:00
JonnyWong16
9f964b5a87 Move notification agent instructions to wiki 2018-03-18 17:05:30 -07:00
JonnyWong16
2245e38d40 Retrieve newsletter using uuid 2018-03-18 00:45:27 -07:00
JonnyWong16
c9618322c2 Log newsletter start/end date and uuid 2018-03-17 23:58:39 -07:00
JonnyWong16
960e147e10 Update Arrow to 0.10.0 2018-03-17 23:08:06 -07:00
JonnyWong16
bbca0b3b42 Add newsletter template folder config option 2018-03-17 20:32:55 -07:00
JonnyWong16
ed0b41cd19 Add punctuation to Arnold 2018-03-17 18:44:15 -07:00
JonnyWong16
dc87591992 Show historical stream data (Fixes Tautulli/Tautulli-Issues#27) 2018-03-17 16:36:24 -07:00
JonnyWong16
1f7be7a4d5 Offload image processing to the Plex server 2018-03-17 14:03:27 -07:00
JonnyWong16
d05e80e573 Make sure all exisiting environment variables are included for scripts 2018-03-17 13:30:12 -07:00
JonnyWong16
003e890844 Some small fixes after rebasing 2018-03-17 11:04:17 -07:00
JonnyWong16
afa16cd656 Newsletter preview by default 2018-03-17 10:42:00 -07:00
JonnyWong16
9aff61f670 Add wait message to newsletter loader 2018-03-17 10:42:00 -07:00
JonnyWong16
8b1c7df3ce Add generating newsletter loader 2018-03-17 10:42:00 -07:00
JonnyWong16
25355f29ce Add server name to newsletter 2018-03-17 10:41:18 -07:00
JonnyWong16
09ea81ccd2 Refactor some newsletter code 2018-03-17 10:41:18 -07:00
JonnyWong16
28efaf73c7 Patch apscheduler weekday (Sunday=0) 2018-03-17 10:41:18 -07:00
JonnyWong16
0057481efb Add funcsigs 1.0.2 2018-03-17 10:41:18 -07:00
JonnyWong16
827b012978 Fix float clear for odd newsletter card 2018-03-17 10:41:18 -07:00
JonnyWong16
0e419695cf Change newsletter job name 2018-03-17 10:41:18 -07:00
JonnyWong16
46f26cc307 Add message for missing Pillow library 2018-03-17 10:41:18 -07:00
JonnyWong16
46f7a92c97 Schedule newsletters 2018-03-17 10:39:30 -07:00
JonnyWong16
2a24ea4cdf Patch apscheduler to use datetime.isoweekday() 2018-03-17 10:39:30 -07:00
JonnyWong16
8e13bf4f93 Update apscheduler 3.5.0 2018-03-17 10:36:04 -07:00
JonnyWong16
aa844b76fc Sort newsletter libraries when rendered 2018-03-17 10:36:04 -07:00
JonnyWong16
0e5bb7b188 Add select/remove all for newsletter libraries 2018-03-17 10:36:04 -07:00
JonnyWong16
49a6cf8809 Add newsletter star rating on hover 2018-03-17 10:36:04 -07:00
JonnyWong16
2adad24684 Fix import on newsletter templates 2018-03-17 10:36:04 -07:00
JonnyWong16
d4d5ff9de7 Add newsletter handler 2018-03-17 10:36:04 -07:00
JonnyWong16
33c2315384 Add view on Plex overlay on newsletter 2018-03-17 10:34:08 -07:00
JonnyWong16
4577704f19 Redo newsletter CSS 2018-03-17 10:34:08 -07:00
JonnyWong16
a13d93f239 Add selectize input for email for newsletters 2018-03-17 10:31:20 -07:00
JonnyWong16
5ac5b3cd29 Add email subject line and sending newsletters 2018-03-17 10:22:04 -07:00
JonnyWong16
d104ec216c Fix newsletter save new email config 2018-03-17 10:22:04 -07:00
JonnyWong16
32645c374e Change to include libraries instead of exclude 2018-03-17 10:22:04 -07:00
JonnyWong16
d1f982847b Add exclude libraries option to newsletter
* Remove individual media type toggles
2018-03-17 10:22:04 -07:00
JonnyWong16
7770431b67 Add email config to newsletters 2018-03-17 10:22:04 -07:00
JonnyWong16
edeb6ae4e4 Add season count to newsletter tv cards 2018-03-17 10:19:28 -07:00
JonnyWong16
af3501a6a6 Add view on Plex to newsletter 2018-03-17 10:19:28 -07:00
JonnyWong16
0f39201774 Initial newsletter support 2018-03-17 10:17:39 -07:00
JonnyWong16
b73d2ff1f7 Add shared libraries for admin user 2018-03-17 10:11:49 -07:00
JonnyWong16
6009fb24b6 Add server token and shared libraries to user refresh 2018-03-17 10:11:49 -07:00
JonnyWong16
522684b2ab v2.0.23-beta 2018-03-16 19:59:06 -07:00
JonnyWong16
feab16b351 Update API docs for get_server_id 2018-03-16 19:47:49 -07:00
JonnyWong16
ee041db63d Pass common environment variable to scripts 2018-03-16 18:37:50 -07:00
JonnyWong16
2479533d07 Show Plex Server URL in settings 2018-03-16 17:43:32 -07:00
JonnyWong16
d045fd5834 Update Facebook Graph API version 2018-03-16 15:39:41 -07:00
JonnyWong16
8407f27fed Add value3 to IFTTT notifications (Closes #1279) 2018-03-16 09:45:30 -07:00
JonnyWong16
b505286caf Add season/episode/album/track count to notification parameters 2018-03-16 09:42:32 -07:00
JonnyWong16
feb762ce8b Beta/nightly update check to include non-beta releases 2018-03-16 08:37:50 -07:00
JonnyWong16
8acdb5af83 Use media stream info for transcode decision (Fixes Tautulli/Tautulli-Issues#24) 2018-03-14 19:45:47 -07:00
JonnyWong16
5af1294f71 Make websocket thread daemon 2018-03-14 16:19:22 -07:00
JonnyWong16
87d2d273d3 Attempt at fixing custom condition json error 2018-03-13 22:16:23 -07:00
JonnyWong16
b5c52ac71e Add logging for failed custom condition json 2018-03-13 20:45:41 -07:00
JonnyWong16
efe9a15f72 Cast Email username/password to string 2018-03-13 20:41:07 -07:00
JonnyWong16
525f1e4b0b Use cherrypy remote for login IP info 2018-03-13 10:00:08 -07:00
JonnyWong16
d18820b832 Use cherrypy base for login host info 2018-03-13 09:42:01 -07:00
JonnyWong16
7e024fd736 Remove test comment in c9c5989 2018-03-13 09:09:27 -07:00
JonnyWong16
c9c5989474 Fix login logs for Plex admin user 2018-03-13 09:08:09 -07:00
JonnyWong16
ce9f96d3be Exit if failed to move database instead of continuing 2018-03-12 19:43:46 -07:00
JonnyWong16
7362dd0bf4 Close websocket cleanly on shutdown 2018-03-12 19:38:19 -07:00
JonnyWong16
9905ebc144 Don't empty results if message in API response (Fixes Tautulli/Tautulli-Issues#13) 2018-03-12 08:56:43 -07:00
JonnyWong16
8f8010884b Add git pull after checkout from interface 2018-03-12 08:20:20 -07:00
JonnyWong16
37afd141be Catch invalid json for custom conditions 2018-03-11 20:59:18 -07:00
JonnyWong16
a3643b4302 Fix typos 2018-03-10 20:54:21 -08:00
JonnyWong16
02cfd8d9b7 Fix git branch select box height 2018-03-10 20:33:18 -08:00
JonnyWong16
941ce439b4 Update API message for remote app settings 2018-03-10 18:03:23 -08:00
JonnyWong16
a08bce2073 v2.0.22 2018-03-10 09:32:08 -08:00
JonnyWong16
4e9c8322c3 Don't overwrite tautulli db on move 2018-03-10 09:32:05 -08:00
JonnyWong16
89bfe85be3 Workaround for duration reported as minutes for a show 2018-03-10 08:58:15 -08:00
JonnyWong16
98d994591c Fix runtime round to minutes 2018-03-09 19:12:12 -08:00
JonnyWong16
a29bc7f4f9 v2.0.22-beta 2018-03-09 17:58:40 -08:00
JonnyWong16
288f4c5f7f Fix expanding selectize box 2018-03-09 15:50:53 -08:00
JonnyWong16
a6bf78ed56 Check is schedulers running before shutdown 2018-03-08 18:32:47 -08:00
JonnyWong16
8dbb05931e Fix library refresh when missing library 2018-03-08 18:23:12 -08:00
JonnyWong16
ac8a712ff0 Fix refreshing activity after losing connection 2018-03-06 20:01:11 -08:00
JonnyWong16
39406c25c3 Add retry and expire for Pushover priority 2 2018-03-06 09:57:06 -08:00
JonnyWong16
48d7c2c54c Fix photo library count and media info table 2018-03-05 09:49:48 -08:00
JonnyWong16
0217188274 Fix update check 2018-03-04 22:49:32 -08:00
JonnyWong16
fd762e71de Fix cherrypy sending wrong Content-Type header for svg 2018-03-04 22:32:26 -08:00
JonnyWong16
4d5c3b6df0 v2.0.21-beta 2018-03-04 14:51:27 -08:00
JonnyWong16
7df54e4d1b Replace Flattr with Patreon 2018-03-04 14:25:38 -08:00
JonnyWong16
5d085de9d3 Rename logger name 2018-03-04 12:24:25 -08:00
JonnyWong16
a8a4299086 Add execute permission to PlexPy.py 2018-03-04 12:17:09 -08:00
JonnyWong16
86f0e8425c Add execute permission to Tautulli.py 2018-03-04 12:15:05 -08:00
JonnyWong16
d2e879be4a Add PlexPy.py file to run Tautulli.py 2018-03-04 12:01:31 -08:00
JonnyWong16
544114fffe Rename css files to tautulli 2018-03-04 11:40:38 -08:00
JonnyWong16
3b3e207b11 Rename log files to tautulli 2018-03-04 11:38:31 -08:00
JonnyWong16
84aad638ac Rename database backup to tautulli 2018-03-04 11:28:35 -08:00
JonnyWong16
2bb691966e Rename default notifier settings to tautulli 2018-03-04 11:18:04 -08:00
JonnyWong16
8f5e788270 Rename plexpy.db to tautulli.db 2018-03-04 11:17:35 -08:00
JonnyWong16
7c43ea2f46 Rename PlexPy.py to Tautulli.py 2018-03-04 11:17:11 -08:00
JonnyWong16
8146e1e3cf Capitalize Tautulli folder in init scripts 2018-03-04 10:28:28 -08:00
JonnyWong16
51b1ff6d4a Rename variables in Ubuntu script 2018-03-04 10:17:16 -08:00
JonnyWong16
403e8dfbea Update all init scripts to Tautulli 2018-03-04 09:44:02 -08:00
JonnyWong16
9d08717c83 Fix missing country in whois lookup causing error 2018-03-02 15:39:05 -08:00
JonnyWong16
66167d5960 Remove word "allowed" 2018-03-02 10:24:28 -08:00
JonnyWong16
624863d826 Hide number input spinners on Firefox 2018-03-02 08:48:31 -08:00
JonnyWong16
d4b3810fbc Reduce number input width 2018-03-01 19:34:52 -08:00
JonnyWong16
6056e1d3b9 Hide arrows on number inputes 2018-03-01 13:08:43 -08:00
JonnyWong16
1a293d525f Update database session on state change 2018-02-28 13:34:21 -08:00
JonnyWong16
b87eb68bdd Identify if a stream is using Plex Relay 2018-02-27 20:03:31 -08:00
JonnyWong16
8620546d07 Move import from a082109 2018-02-27 15:17:35 -08:00
JonnyWong16
a082109045 Don't ping for activity if websocket is not connected 2018-02-27 15:02:17 -08:00
JonnyWong16
559a9b393e Catch failure to send analytics event 2018-02-24 15:08:58 -08:00
JonnyWong16
ae41b22e59 Forgot one version number in 754fd24 2018-02-24 14:51:19 -08:00
JonnyWong16
754fd24421 Refactor some code 2018-02-24 10:09:02 -08:00
JonnyWong16
ab34a74210 v2.0.20-beta 2018-02-24 09:22:47 -08:00
JonnyWong16
cfa6de4d91 Remove content group 2018-02-24 09:02:46 -08:00
JonnyWong16
a5608c7a1e Revert to png for logos 2018-02-22 19:30:10 -08:00
JonnyWong16
88a7b52e51 Add content group metric dev/production 2018-02-22 12:39:32 -08:00
JonnyWong16
e444bad4de Switch metric dimensions 2018-02-22 12:28:22 -08:00
JonnyWong16
5403b0b547 Install or update event 2018-02-22 11:41:22 -08:00
JonnyWong16
51b5e615f5 Add some more system metrics 2018-02-22 09:20:58 -08:00
JonnyWong16
700547b63b Separate system analytics 2018-02-22 08:12:15 -08:00
JonnyWong16
3f3d1962c7 Hash client ID 2018-02-22 07:29:56 -08:00
JonnyWong16
655a359ef4 Clean up tracker 2018-02-22 07:28:13 -08:00
JonnyWong16
90647628c9 Test sending install metrics on startup 2018-02-21 10:28:03 -08:00
JonnyWong16
681c3ed6e3 Add Google Universal Analytics 2018-02-21 10:27:28 -08:00
JonnyWong16
7f255943c6 Commit to link to the commit 2018-02-20 15:48:50 -08:00
JonnyWong16
b6e73b5dea Fix fallback thumb for home stats cards 2018-02-20 13:39:41 -08:00
JonnyWong16
eacb7f6ae5 Proper image name for poster uploads 2018-02-19 19:38:36 -08:00
JonnyWong16
7b300bb87e Add "Note" tag for Imgur and 3rd party API message 2018-02-19 19:19:27 -08:00
JonnyWong16
a81ad27d85 Add include subject line for Pushover 2018-02-19 19:10:08 -08:00
JonnyWong16
8eed14ff3b Add posters to Pushbullet notifications 2018-02-19 19:08:52 -08:00
JonnyWong16
82446acdf0 Telegram upload image in single message 2018-02-19 19:05:44 -08:00
JonnyWong16
88770b8805 Imgur upload not required for Pushover posters 2018-02-19 12:36:12 -08:00
JonnyWong16
f9f05bbea3 Add posters to Pushover notifications 2018-02-19 12:32:35 -08:00
JonnyWong16
17dd767c22 Send HipChat header 2018-02-19 11:57:21 -08:00
JonnyWong16
25b1dc6dd8 Fix refresh login logs on user page 2018-02-19 11:42:14 -08:00
JonnyWong16
b2b1277e37 Don't reload table again when switching tabs on user and library pages 2018-02-19 10:59:40 -08:00
JonnyWong16
8e1a588ced Fix conflicting history and sync delete mode on user page 2018-02-19 10:59:10 -08:00
JonnyWong16
9eddfafeae Correct poster height on the watch statistic cards 2018-02-19 10:05:19 -08:00
JonnyWong16
d24a922ccb Adjust media screen size for button bar 2018-02-19 10:00:17 -08:00
JonnyWong16
bbc6482c99 Add edit mode to sync table on user page 2018-02-19 09:11:35 -08:00
JonnyWong16
36ff1fb674 Fix button layout on mobile site 2018-02-19 08:39:27 -08:00
JonnyWong16
f0aa793262 Update wording for group history setting 2018-02-18 13:01:09 -08:00
JonnyWong16
681627a656 Fix user filtering on graphs with grouping 2018-02-18 12:58:40 -08:00
JonnyWong16
87c6ad66fb Add grouping to the remaining graphs 2018-02-18 12:50:28 -08:00
JonnyWong16
4ab9eb3bfa Fix popovers in history table modal 2018-02-18 12:15:11 -08:00
JonnyWong16
2d56ac027b Add plays graph grouping to API docs 2018-02-18 11:06:17 -08:00
JonnyWong16
836c4293d6 Respect group history setting in graphs 2018-02-18 11:04:31 -08:00
JonnyWong16
07092e8aa5 Don't reconnect server when saving settings if server settings are not changed 2018-02-18 11:03:57 -08:00
JonnyWong16
66743c1401 Add conditions bypass message for manual recently added notification trigger 2018-02-18 08:41:55 -08:00
JonnyWong16
bfe34e060b Fix KeyError from 868aeb3 2018-02-18 08:36:53 -08:00
JonnyWong16
5ed4236a22 "Commit" if only one commit behind 2018-02-18 08:33:14 -08:00
JonnyWong16
868aeb3902 Fix notification update parameter types 2018-02-18 08:29:40 -08:00
JonnyWong16
cbcdac5b04 Update message to show release instead of commits for master and beta 2018-02-18 08:28:44 -08:00
JonnyWong16
d473bb3058 Prevent dismissing the modal on the shutdown page 2018-02-17 15:05:54 -08:00
JonnyWong16
066a95d209 v2.0.19-beta 2018-02-16 22:22:51 -08:00
JonnyWong16
c7cc476623 Change "Close" to "Dismiss" in update bar 2018-02-16 15:24:48 -08:00
JonnyWong16
bd44eb7fe4 Redraw table after refresh 2018-02-16 11:22:39 -08:00
JonnyWong16
6ec4f51077 Don't delete session cache folder on startup 2018-02-16 11:18:56 -08:00
JonnyWong16
b4a4f60b04 Fix manual refreshing the libraries/users list 2018-02-16 11:17:30 -08:00
JonnyWong16
dc4e6edc9a Fix update bar dismiss if it was not shown originally 2018-02-16 11:00:01 -08:00
JonnyWong16
60b362b19e Transparent update bar 2018-02-16 10:58:00 -08:00
JonnyWong16
7e81ce8c06 Fade in/out update message 2018-02-16 10:47:59 -08:00
JonnyWong16
c7f9e2f721 Change update bar css 2018-02-16 10:31:22 -08:00
JonnyWong16
cab8b1c041 Check for updates without refreshing the page 2018-02-16 10:24:55 -08:00
JonnyWong16
16f270691d Check on watched notification states before adding to the queue 2018-02-15 15:16:50 -08:00
JonnyWong16
d94a1efe75 Add media info table refresh to API docs 2018-02-15 15:15:36 -08:00
JonnyWong16
12755970b7 Fix failure to make session cache folder on startup 2018-02-15 12:21:44 -08:00
JonnyWong16
93e4853ea2 Fix delete media info cache 2018-02-14 11:19:53 -08:00
JonnyWong16
5e0c0365fb Change button colours on setup wizard 2018-02-14 09:48:04 -08:00
JonnyWong16
c2713c53dd Only connect if first run is complete 2018-02-14 08:53:49 -08:00
JonnyWong16
90443b4028 Catch failed to retrieve Plex Cloud status 2018-02-14 08:53:27 -08:00
JonnyWong16
e0109ed179 Combine connection function for cloud and non-cloud servers 2018-02-14 08:45:45 -08:00
JonnyWong16
a53afe05a2 Check cloud status on startup before connecting websocket 2018-02-14 06:55:44 -08:00
JonnyWong16
a5d2467bfe Less log spam of cloud server status 2018-02-14 06:39:42 -08:00
JonnyWong16
8447663e27 Fix server up/down status on Tautulli startup 2018-02-14 06:35:59 -08:00
JonnyWong16
64d67d8209 Hide remote access check message 2018-02-13 22:48:28 -08:00
JonnyWong16
78034b82a9 Send Use SSL and Remote Server checkbox values when disabled 2018-02-13 22:06:09 -08:00
JonnyWong16
f77bd6c17b Move server selectize dropdown container 2018-02-13 19:30:20 -08:00
JonnyWong16
2621da7d36 Add server selection dropdown to settings 2018-02-13 19:22:11 -08:00
JonnyWong16
e1dca1509a Reconnect Plex Cloud without keeping the server awake 2018-02-13 10:49:11 -08:00
JonnyWong16
df016243dd Refactor some websocket connection code 2018-02-13 08:48:54 -08:00
JonnyWong16
be72693fec Catch WebSocketException when attempting to reconnect 2018-02-13 07:08:35 -08:00
JonnyWong16
33a1ebdb1a Show location for masked session info 2018-02-12 17:40:11 -08:00
JonnyWong16
030f9d334b Improve server selectize on setup wizard 2018-02-12 17:33:35 -08:00
JonnyWong16
dc743ac378 Fix show full changelog on fresh install 2018-02-12 17:16:43 -08:00
JonnyWong16
0010cbe21f Update masked info for guest access 2018-02-12 11:35:34 -08:00
JonnyWong16
3a5d5918de v2.0.18-beta 2018-02-12 09:44:57 -08:00
JonnyWong16
3380e39de2 Add button to delete 3rd party API lookup info 2018-02-12 09:31:44 -08:00
JonnyWong16
7d31079897 Change group history table on by default 2018-02-12 08:20:58 -08:00
JonnyWong16
c287b6df77 Fix DepreciationWarning error for URIs with query string parameters 2018-02-12 08:15:21 -08:00
JonnyWong16
dab1f8ba20 Save The Movie Database info after lookup 2018-02-11 22:03:15 -08:00
JonnyWong16
a26de7f6c2 Move repository 2018-02-11 20:23:37 -08:00
JonnyWong16
ab32b2cbc2 Compressed screenshot for readme 2018-02-11 19:15:20 -08:00
JonnyWong16
503c249fc3 Update API docs 2018-02-11 19:11:31 -08:00
JonnyWong16
2a03ce757e Add toggle for advanced settings 2018-02-11 17:55:37 -08:00
JonnyWong16
373a15524f Remove redundant settings headers 2018-02-11 17:17:08 -08:00
JonnyWong16
13036183d3 Move extra settings to other tabs 2018-02-11 16:38:21 -08:00
JonnyWong16
170591c79e Move some settings, split notifications agents back out 2018-02-11 16:11:00 -08:00
JonnyWong16
a15d225a5f Fix missing Host in login logs for Firefox 2018-02-10 12:23:05 -08:00
JonnyWong16
a0106874e2 Fix paused and resume notifications only triggering once 2018-02-08 21:19:43 -08:00
JonnyWong16
ab157d1c0e Fix default text on Tautulli update notification 2018-02-08 12:21:21 -08:00
JonnyWong16
0b95c9fe2e Add Imgur poster deletion 2018-02-07 17:59:08 -08:00
JonnyWong16
d693514ca9 Notification exclusion tags change media item to media type 2018-02-07 11:44:16 -08:00
JonnyWong16
56987b3aaa Add note to notification exclusion tags 2018-02-07 11:42:17 -08:00
JonnyWong16
3ca1bd5d78 Change custom conditions negative operators to "and" 2018-02-03 17:02:13 -08:00
JonnyWong16
5d2219f2f8 v2.0.17-beta 2018-02-03 09:35:05 -08:00
JonnyWong16
56dc28eed3 Clear session metadata cache on startup 2018-02-03 09:06:05 -08:00
JonnyWong16
3e723d4373 Fix photo album media type 2018-02-02 23:49:38 -08:00
JonnyWong16
f5e341e655 Don't sanitize tags for Slack and Discord 2018-02-02 23:22:41 -08:00
JonnyWong16
3c81100957 Fix media info table sorting 2018-02-02 23:03:48 -08:00
JonnyWong16
304378f93b Add Zapier notification agent 2018-02-01 22:11:33 -08:00
JonnyWong16
de6b6e8124 Check for any falsy value in sync item filters 2018-01-31 08:59:37 -08:00
JonnyWong16
d15223fb1a v2.0.16-beta 2018-01-30 23:20:34 -08:00
JonnyWong16
d29a12b6db Add user filter to the synced table 2018-01-30 23:07:21 -08:00
JonnyWong16
9100e25a21 Pass copy of notification data to prevent multithreading issues 2018-01-30 23:04:44 -08:00
JonnyWong16
7672f1955e Fix sync table not loading 2018-01-30 21:19:37 -08:00
JonnyWong16
5f52171fc4 Add "Use Server Setting" as Plex update channel 2018-01-30 19:56:48 -08:00
JonnyWong16
31ac82ad71 Comment out logging for writing session history to database 2018-01-30 19:06:10 -08:00
JonnyWong16
38ca4e37a6 Fix matching of synced playback 2018-01-30 19:04:30 -08:00
JonnyWong16
3c55550702 Add logging for writing session history to database 2018-01-30 10:04:28 -08:00
JonnyWong16
7dff6b121b Log force stopped message 2018-01-30 09:31:13 -08:00
JonnyWong16
d77d889695 Fix activity callback function argument 2018-01-30 09:13:06 -08:00
JonnyWong16
318a21438f Fix sometimes time showing as "0:60" 2018-01-28 20:19:49 -08:00
JonnyWong16
7175b57a28 Fix "unknown" stream resolution in graphs 2018-01-28 10:06:47 -08:00
JonnyWong16
e1e5a050c2 v2.0.15-beta 2018-01-27 11:08:45 -08:00
JonnyWong16
58996c1115 Unused now time 2018-01-27 10:59:48 -08:00
JonnyWong16
7301fe5f6e Remove 24 hour limit for recently added 2018-01-26 12:29:38 -08:00
JonnyWong16
a27c423569 Line up cards on the homepage 2018-01-24 21:37:02 -08:00
JonnyWong16
19680d3bc7 Refresh stream location on activity cards 2018-01-24 21:14:18 -08:00
JonnyWong16
ecaca4e5dc Change hover text from "View in" to "View on" 2018-01-24 21:07:12 -08:00
JonnyWong16
191de0b577 Add "View On" to Plex Web click-through 2018-01-24 21:04:34 -08:00
JonnyWong16
ebcc073b32 Add more server notification parameters. Rename plexpy parameters to tautulli. 2018-01-22 17:50:48 -08:00
JonnyWong16
043b3fd57b Update state for "Check server response" task 2018-01-22 13:44:51 -08:00
JonnyWong16
dd50502dcb Update Discord link to welcome channel 2018-01-22 11:27:04 -08:00
JonnyWong16
f159a1014d Don't add view_offset to live progress bar 2018-01-21 19:46:23 -08:00
JonnyWong16
abb801535c Add line break for Live progress 2018-01-21 16:09:48 -08:00
JonnyWong16
2732dbf1b1 Fix progress time for live tv 2018-01-21 16:07:32 -08:00
JonnyWong16
095d893005 Improve Live TV info on activity cards 2018-01-21 15:54:38 -08:00
JonnyWong16
5d8455d141 Get rating key for live sessions from websocket data 2018-01-21 13:09:02 -08:00
JonnyWong16
aa3450bfcc Add Labels and Collections to notification parameters 2018-01-20 20:01:01 -08:00
JonnyWong16
770f12b632 Hide advanced settings 2018-01-20 14:03:23 -08:00
JonnyWong16
45c2ccdffe v2.0.14-beta 2018-01-20 11:42:36 -08:00
JonnyWong16
fc14c3165f Remove email line break message 2018-01-20 11:30:11 -08:00
JonnyWong16
0fad245148 Try to cleanly shutdown loggers 2018-01-20 11:27:58 -08:00
JonnyWong16
79609c384e Show all changelogs when updated since previous version 2018-01-20 10:27:08 -08:00
JonnyWong16
09054ddb4b Correct clear logs message 2018-01-19 19:11:55 -08:00
JonnyWong16
6f912d4aa2 Add date header to Emails and do not add HTML line breaks automatically 2018-01-19 15:54:58 -08:00
JonnyWong16
96033a8214 Rename Tautulli update notification parameters 2018-01-19 14:59:00 -08:00
JonnyWong16
5ca65f4797 Catch json ValueError in metadata cache 2018-01-19 07:13:53 -08:00
JonnyWong16
d2fccbde68 Json dump custom conditions 2018-01-18 14:02:47 -08:00
JonnyWong16
e6b48d7baf Check for browser proxy compatibility 2018-01-17 21:02:31 -08:00
JonnyWong16
3e51310511 Re-enable browser notifications 2018-01-17 17:01:44 -08:00
JonnyWong16
32b43202c2 Attempt at fixing stuck sessions which require flishing the database 2018-01-15 18:55:37 -08:00
JonnyWong16
446170f8de Reduce websocket logging to playing and timeline only 2018-01-15 17:59:12 -08:00
JonnyWong16
c5a9ecd4ac Make sure websocket events are for library items 2018-01-15 14:49:49 -08:00
JonnyWong16
2af5f817a3 Plex Web url for tracks should go to the album page 2018-01-15 14:37:40 -08:00
JonnyWong16
4e55cf3cd4 Add all other bandwidth to WAN 2018-01-15 14:25:42 -08:00
JonnyWong16
eeb0478813 Use font-awesome arrow on activity cards 2018-01-14 20:45:27 -08:00
JonnyWong16
33739f1cb2 Fix check activity session write success 2018-01-13 21:16:07 -08:00
JonnyWong16
515e6a8071 Sort selectize when rendered 2018-01-13 17:47:24 -08:00
JonnyWong16
2b22f8eb4f Add select/remove all options for emails 2018-01-13 17:18:09 -08:00
JonnyWong16
e9725a0081 v2.0.13-beta 2018-01-13 12:34:57 -08:00
JonnyWong16
8fd159d2fe Use added_at to check recently added more than 24 hours ago 2018-01-12 12:34:07 -08:00
JonnyWong16
3d7e6c8b2c Update circle logo 2018-01-12 09:01:48 -08:00
JonnyWong16
0c048d61b1 Add message that Imgur upload may be required 2018-01-12 01:00:58 -08:00
JonnyWong16
f05b8e5cd1 Add error message when notification subject/body is required 2018-01-12 00:48:53 -08:00
JonnyWong16
0b38fec827 Add more options for Join notifications 2018-01-12 00:35:37 -08:00
JonnyWong16
547dc9ed33 Fix checking login permissions 2018-01-11 23:28:10 -08:00
JonnyWong16
896a37bea9 Allow Plex admin to login as Tautulli admin 2018-01-11 22:58:30 -08:00
JonnyWong16
3f90037db3 Make custom conditions message clearer 2018-01-11 22:52:52 -08:00
JonnyWong16
380ca11ced Fix some parameter types 2018-01-11 22:07:29 -08:00
JonnyWong16
ab3a288e49 Add "or" between condition values 2018-01-10 18:31:14 -08:00
JonnyWong16
638e225f80 Add selectize resources 2018-01-10 00:36:01 -08:00
JonnyWong16
5089ede207 Add selectize for email input 2018-01-10 00:32:24 -08:00
JonnyWong16
a3e6e76158 Implement JWT instead of using cherrypy sessions 2018-01-08 22:25:52 -08:00
JonnyWong16
7c4c7bfc90 Add pyjwt 1.4.0 2018-01-08 22:24:36 -08:00
JonnyWong16
644fea6665 v2.0.12-beta 2018-01-07 23:44:18 -08:00
JonnyWong16
a1349ff8a6 Add css for selectize to match input boxes 2018-01-07 23:37:18 -08:00
JonnyWong16
71c20002b8 Update build notify text error message 2018-01-07 18:45:55 -08:00
JonnyWong16
157af84226 Only update the database sessions every 60 seconds while playing 2018-01-07 17:10:33 -08:00
JonnyWong16
9b4536f132 Move webserver notify to API 2018-01-07 14:46:01 -08:00
JonnyWong16
29ab470e42 Make metadata cache an advanced config option 2018-01-07 10:01:17 -08:00
JonnyWong16
c67fa480a7 Make condition logic optional
* Implicit "and" between all conditions if logic is blank
2018-01-07 09:42:57 -08:00
JonnyWong16
0a1a691c73 Fix Plex URL notification parameter 2018-01-07 08:28:06 -08:00
JonnyWong16
48588f23bf Add LAN/WAN bandwidth to activity header 2018-01-06 23:06:21 -08:00
JonnyWong16
cf14fbc3f0 v2.0.11-beta 2018-01-05 21:50:35 -08:00
JonnyWong16
e471d5207d Remove experimental tag from calculate file sizes 2018-01-05 21:50:24 -08:00
JonnyWong16
5722a52082 Fix None values in stream data for pre v2 history 2018-01-05 21:37:54 -08:00
JonnyWong16
08c32e875e Fix login using hashed password 2018-01-05 21:01:32 -08:00
JonnyWong16
7d3ee3afb3 Fix recently added show title 2018-01-05 21:01:10 -08:00
JonnyWong16
def8600f5c Reload notify params from raw stream info 2018-01-05 14:22:20 -08:00
JonnyWong16
74a68f3c7d v2.0.10-beta 2018-01-04 19:55:13 -08:00
JonnyWong16
64c9247dd1 Remove library/user notification toggles
* Filter out notifications using custom conditions
2018-01-04 19:39:16 -08:00
JonnyWong16
1bfcd34247 Some formatting for common.py 2018-01-04 13:40:34 -08:00
JonnyWong16
19864e97e6 Fix media type in collection header 2018-01-04 13:40:34 -08:00
JonnyWong16
ec5c5e1420 Merge pull request #1195 from Tommatheussen/patch-1
Update date formats
2018-01-04 13:39:25 -08:00
Tom Matheussen
803f4e14ca Added some additional formats 2018-01-04 22:18:40 +01:00
Tom Matheussen
6cc254b80a Update Date Formats
Added correct Year date formats, replaced generic numeric values with actual examples
2018-01-04 21:32:59 +01:00
JonnyWong16
59593ab1aa Fix HW indicator on activity refresh 2018-01-03 20:29:52 -08:00
JonnyWong16
65a0a0eb7d v2.0.9-beta 2018-01-03 19:37:12 -08:00
JonnyWong16
f4206b401f Fix season/episode numbers zfill 2018-01-03 19:24:19 -08:00
JonnyWong16
99f8d24b3e Remove bottom padding on stats info 2018-01-03 16:35:22 -08:00
JonnyWong16
26b06e453d v2.0.8-beta 2018-01-03 16:08:21 -08:00
JonnyWong16
54ab646048 Don't line break product or player on activity cards 2018-01-03 16:02:22 -08:00
JonnyWong16
12c9aa3d6a Try caching metadata for sessions 2018-01-03 13:36:26 -08:00
JonnyWong16
1ae8544f2d Cleanup notification parameters 2018-01-03 11:36:49 -08:00
JonnyWong16
eae9e66c75 Updating missing notification parameters 2018-01-02 16:18:50 -08:00
JonnyWong16
ad041a1691 Attempt to fix HW transcoding indicator 2018-01-02 16:13:27 -08:00
JonnyWong16
1aee3b6c8f Add idna 2.6 2018-01-02 09:03:55 -08:00
JonnyWong16
04d4ffb63d v2.0.7-beta 2018-01-01 18:05:26 -08:00
JonnyWong16
80b318b45c Check for failed to retrieve releases from GitHub 2018-01-01 16:09:26 -08:00
JonnyWong16
19969a8b1f Clickable logo using pointer-events 2018-01-01 11:11:17 -08:00
JonnyWong16
b84888356f Don't decode http response content 2017-12-31 22:13:39 -08:00
JonnyWong16
c9436195f3 Fix session pause timestamp 2017-12-31 20:50:03 -08:00
JonnyWong16
98cfb50571 Fix hub related request 2017-12-31 20:32:52 -08:00
JonnyWong16
b67884ea7f Fix LAN/WAN on activity cards 2017-12-31 20:30:48 -08:00
JonnyWong16
c7081b4417 v2.0.6-beta 2017-12-31 19:48:14 -08:00
JonnyWong16
83f1519932 Beta Plex Cloud support 2017-12-31 19:44:01 -08:00
JonnyWong16
6c6493fe31 Import urllib3 directly 2017-12-31 18:56:08 -08:00
JonnyWong16
1564cdf55e Add chardet 3.0.4 2017-12-31 18:56:08 -08:00
JonnyWong16
ffcb1144b6 Add urllib3 1.22 2017-12-31 18:56:08 -08:00
JonnyWong16
4ad644ab7f Update http handler for new requests and certifi 2017-12-31 18:56:08 -08:00
JonnyWong16
f1783df8d6 Update certifi to 2017.11.05 2017-12-31 18:56:08 -08:00
JonnyWong16
b1a3dd1a46 Update requests to 2.18.4 2017-12-31 18:56:08 -08:00
JonnyWong16
52eac2b437 Import certifi from requests 2017-12-31 18:56:08 -08:00
JonnyWong16
ca20eda606 Update paused time while still paused 2017-12-31 18:37:55 -08:00
JonnyWong16
4e3b95950f Fix history table stopped time showing as "n/a" 2017-12-31 18:36:57 -08:00
JonnyWong16
9d01335395 v2.0.5-beta 2017-12-31 17:52:53 -08:00
JonnyWong16
63814584e9 Add sortcut to /status/sessions 2017-12-31 17:37:07 -08:00
JonnyWong16
bf7e432c0c Add total bandwidth to activity header 2017-12-31 17:13:38 -08:00
JonnyWong16
5a14c229a1 Fix IPv6 overflow on current activity cards 2017-12-31 17:12:49 -08:00
JonnyWong16
79581fb83e Sanitize script output in logger 2017-12-31 14:41:32 -08:00
JonnyWong16
90c587e6e1 Unique session id name to not conflict with multiple installs 2017-12-31 14:33:21 -08:00
JonnyWong16
b2b728a3cc Save cherrypy sessions to disk instead of ram 2017-12-31 14:29:57 -08:00
JonnyWong16
645ef86c75 Fix Join notifications not sending 2017-12-31 14:29:57 -08:00
JonnyWong16
450fda18f1 Merge pull request #1181 from felixbuenemann/fix-cherrypy-ssl-connection-type-deprecation
Fix OpenSSL SSL.ConnectionType deprecation warning
2017-12-31 12:37:22 -07:00
Felix Bünemann
f6162a8df8 Fix OpenSSL SSL.ConnectionType deprecation warning
This patches the vendored cherrypy lib to work around a deprecation
warning in pyOpenSSL 17.1.0 and later by checking and using the
SSL.Connection class if available and falling back to ConnectionType.
2017-12-31 14:30:15 +01:00
JonnyWong16
a8c5f48b60 Merge branch 'nightly' of https://github.com/JonnyWong16/plexpy into nightly 2017-12-30 23:22:02 -08:00
JonnyWong16
edc0b96167 Some sync data updates 2017-12-30 23:21:31 -08:00
JonnyWong16
9b4b3e0ecb Add stopped state to history table activity 2017-12-30 17:27:59 -08:00
JonnyWong16
7bd2c00636 Fix presence in get_server_resources 2017-12-30 17:27:50 -08:00
JonnyWong16
15faccfa2f Update server connection code 2017-12-30 17:27:39 -08:00
JonnyWong16
11e10e7d0e Update resources links 2017-12-30 16:13:55 -08:00
JonnyWong16
8ef48fc548 Update svg logo with shadows 2017-12-30 14:14:47 -08:00
JonnyWong16
29632b0805 Validate git releases as a list 2017-12-29 16:49:55 -08:00
JonnyWong16
e908c45cf2 v2.0.4-beta 2017-12-29 16:35:24 -08:00
JonnyWong16
c37e934f42 Check concurrent streams from the server instead of database 2017-12-29 16:29:20 -08:00
JonnyWong16
41f91956b8 Make sure current activity is loaded before refreshing 2017-12-29 16:26:50 -08:00
JonnyWong16
8b2bd5ce79 Add collections info pages 2017-12-29 15:52:08 -08:00
JonnyWong16
28e4151157 Remember tab when going back on library and user pages 2017-12-29 11:34:41 -08:00
JonnyWong16
66d45293e6 Add test Plex Web URL button 2017-12-29 10:47:31 -08:00
JonnyWong16
243eeeff67 v2.0.3-beta 2017-12-25 09:58:24 -08:00
JonnyWong16
18520e24d1 Report correct release version on beta 2017-12-25 09:48:33 -08:00
JonnyWong16
87743171b7 Fix write session history optimized_version_title column name 2017-12-25 09:07:02 -08:00
JonnyWong16
452f0747c8 Add missing stream info modal css 2017-12-25 09:02:33 -08:00
JonnyWong16
a20747044c Fix sync ID 2017-12-25 09:00:42 -08:00
JonnyWong16
e60241ca73 v2.0.2-beta 2017-12-24 13:53:49 -08:00
JonnyWong16
c69e68a4a8 Alias media info title to sort title 2017-12-24 11:29:12 -08:00
JonnyWong16
561d994328 Fix config version updgrade 2017-12-24 07:52:11 -08:00
JonnyWong16
33acf9402d Improved synced and optimized version info 2017-12-23 11:52:28 -08:00
JonnyWong16
0a42cb4135 Update stream info modal 2017-12-22 22:04:40 -08:00
JonnyWong16
19695c220b Fix some missing subtitle database columns 2017-12-22 19:55:21 -08:00
JonnyWong16
1fbc0165e6 Fix source video/audio/subtitle parameters in notifications 2017-12-22 14:22:37 -08:00
JonnyWong16
4dd05e217a Add back HTTP Proxy setting 2017-12-22 13:01:25 -08:00
JonnyWong16
b77a409414 Add sort title to metadata, sort media info tables by sort title 2017-12-22 12:40:58 -08:00
JonnyWong16
b2157df026 Default blank session value when writing session history 2017-12-22 11:47:28 -08:00
JonnyWong16
0b8d5954f9 Fix librariaes page 2017-12-22 10:30:45 -08:00
JonnyWong16
0d60c7b728 Remove unused logos 2017-12-21 15:30:24 -08:00
JonnyWong16
de58136314 Do not send recently added notification if item updated more than 24 hours ago 2017-12-21 15:22:18 -08:00
JonnyWong16
4e718056b6 Remove bif thumbnail setting 2017-12-21 11:23:27 -08:00
JonnyWong16
480913362e Fix default setup wizard settings 2017-12-21 11:23:11 -08:00
JonnyWong16
ad2f61132a Change logos to svgs 2017-12-21 10:49:14 -08:00
JonnyWong16
0e2df4ba14 Add note to enable 3rd party APIs for link lookup 2017-12-21 10:47:32 -08:00
JonnyWong16
c342273742 Retrieve session by session key 2017-12-19 22:20:24 -08:00
JonnyWong16
0cb147cd8f Cleanup safari-pinned-tab svg 2017-12-19 21:49:09 -08:00
JonnyWong16
11b2a17fb3 Fix white in logo text 2017-12-19 21:36:11 -08:00
JonnyWong16
23486ff235 Update favicons 2017-12-19 21:29:07 -08:00
JonnyWong16
a9e24bc0c5 Remove update section ids 2017-12-19 20:36:24 -08:00
JonnyWong16
3c22496ea2 Fix some platform mappings 2017-12-19 20:28:40 -08:00
JonnyWong16
b8164ca556 Validate condition logic on save 2017-12-19 17:32:37 -08:00
JonnyWong16
19565b3d0a Add message to verify server for error communicating 2017-12-19 17:27:20 -08:00
JonnyWong16
50ce17ac72 Fix missing users table cache parameter 2017-12-19 16:40:45 -08:00
JonnyWong16
e51a425389 API enabled by default 2017-12-19 11:47:59 -08:00
JonnyWong16
3df7642193 Missing save button in settings 2017-12-19 11:21:07 -08:00
JonnyWong16
38ad9c9893 v2.0.1-beta 2017-12-19 10:56:25 -08:00
JonnyWong16
fb75b9566f Temporarily disable browser notifications 2017-12-19 10:33:42 -08:00
JonnyWong16
80a6118bf9 Fix music stats background fallback 2017-12-19 09:42:11 -08:00
JonnyWong16
6b2bc0108b Fix Join apikey 2017-12-18 23:09:50 -08:00
JonnyWong16
bd07cf5a04 Missing video_height database column 2017-12-18 13:51:14 -08:00
405 changed files with 62067 additions and 20208 deletions

3
.gitignore vendored
View File

@@ -12,9 +12,12 @@
*.db*
*.db-journal
*.ini
release.lock
version.lock
logs/*
backups/*
cache/*
newsletters/*
*.mmdb
# HTTPS Cert/Key #

856
API.md

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,420 @@
# Changelog
## v2.1.4 (2018-05-05)
* Newsletters:
* Fix: Newsletter URL without an HTTP root.
## v2.1.3-beta (2018-05-04)
* Newsletters:
* Fix: HTTP root doubled in newsletter URL.
* Fix: Configuration would not open with failed hostname resolution.
* Fix: Schedule one day off when using weekday names in cron.
* Fix: Images not refreshing when changed in Plex.
* Fix: Cloudinary upload with non-ASCII image titles.
* Other:
* Fix: Potential XSS vulnerability in search.
## v2.1.2-beta (2018-05-01)
* Newsletters:
* New: Added Cloudinary option for image hosting.
* Notifications:
* New: Added Message-ID to Email header (Thanks @Dam64)
* Fix: Posters not showing up on Twitter with self-hosted images.
* Fix: Incorrect action parameter for new device notifications.
* Change: Hardcode Pushover sound list instead of fetching the list every time.
* API:
* Fix: Success result for empty response data.
* Change: Do not send notification when checking for Tautulli updates via the API.
## v2.1.1-beta (2018-04-11)
* Monitoring:
* Fix: Live TV transcoding showing incorrectly as direct play.
* Newsletters:
* New: Added week number as parameter. (Thanks @samip5)
* Fix: Fallback to cover art on the newsletter cards.
* Change: Option to set newsletter time frame by calendar days or hours.
* Notifications:
* New: Added week number as parameter. (Thanks @samip5)
* Other:
* New: Added plexapi library for custom scripts.
## v2.1.0-beta (2018-04-07)
* Newsletters:
* New: A completely new scheduled newsletter system.
* Beautiful HTML formatted newsletter for recently added movies, TV shows, or music.
* Send newsletters on a daily, weekly, or monthly schedule to your users.
* Customize the number of days of recently added content and the libraries to include on the newsletter.
* Add a custom message to be included on the newsletter.
* Option to either send an HTML formatted email, or a link to a self-hosted newsletter on your own domain to any notification agent.
* Notifications:
* New: Ability to use self-hosted images on your own domain instead of using Imgur.
## v2.0.28 (2018-04-02)
* Monitoring:
* Fix: Homepage activity header text.
## v2.0.27 (2018-04-02)
* Monitoring:
* Change: Move activity refresh interval setting to the settings page.
## v2.0.26-beta (2018-03-30)
* Monitoring:
* New: Setting to change the refresh interval on the homepage.
* Fix: Identify extras correctly on the activity cards.
* Notifications:
* Change: Send Telegram image and text separately if the caption is longer than 200 characters.
* UI:
* Fix: Error when clicking on synced playlist links.
## v2.0.25 (2018-03-22)
* Monitoring:
* Fix: Websocket not reconnecting causing activity monitoring and notifications to not work.
* Fix: Error checking for synced streams without Plex Pass.
## v2.0.24 (2018-03-18)
* Monitoring:
* Fix: Fix stream data not showing for history recorded before v2.
* Notifications:
* Fix: Set all environment variables for scripts.
* Change: Moved all notification agent instructions to the wiki.
* Change: XBMC notification agent renamed to Kodi.
* Change: OSX Notify notification agent renamed to macOS Notification Center.
## v2.0.23-beta (2018-03-16)
* Monitoring:
* Fix: Certain transcode stream showing incorrectly as direct play in history. Fix is not retroactive.
* Notifications:
* New: Added season/episode/album/track count to notification parameters.
* New: Added "Value 3" setting for IFTTT notifications.
* New: Set PLEX_URL, PLEX_TOKEN, TAUTULLI_URL, and TAUTULLI_APIKEY environment variables for scripts.
* Fix: Notifications failing to send with invalid custom conditions json.
* Fix: Email notifications failing with unicode username/passwords.
* Change: Facebook Graph API version updated to v2.12.
* UI:
* New: Show the Plex Server URL in the settings.
* Fix: Incorrect info displayed in the Tautulli login logs.
* API:
* Fix: API returning empty data if a message was in the original data.
* Change: get_server_id command returns json instead of string.
* Other:
* Fix: Forgot git pull when changing branches in the web UI.
## v2.0.22 (2018-03-10)
* Tautulli v2 release!
## v2.0.22-beta (2018-03-09)
* Notifications:
* Fix: Pushover notifications failing with priority 2 is set.
* Fix: Expanding selectize box for some notification agent settings.
* Other:
* Fix: Update check failing when an update is available.
* Fix: Item count incorrect for photo libraries.
## v2.0.21-beta (2018-03-04)
* Monitoring:
* New: Identify if a stream is using Plex Relay.
* Change: Don't ping the Plex server if the websocket is disconnected.
* Notifications:
* Fix: Pause/resume state not being sent correctly in some instances.
* Other:
* New: Add Patreon donation method.
* Fix: Catch failure to send analytics.
* Fix: IP address connection lookup error when the country is missing.
* Change: Updated all init scripts to Tautulli.
* Change: Move database to tautulli.db.
* Change: Move logs to tautulli.log.
* Change: Move startup file to Tautulli.py.
## v2.0.20-beta (2018-02-24)
* Notifications:
* New: Add poster support for Pushover notifications.
* New: Add poster support for Pushbullet notifications.
* Fix: Incorrect Plex/Tautulli update notification parameter types.
* Change: Poster and text sent as a single message for Telegram.
* Change: Posters uploaded directly to Telegram without Imgur.
* UI:
* New: Add "Delete" button to synced items table on user pages.
* Fix: Button spacing/positioning on mobile site.
* Fix: Music statistic cards not using the fallback thumbnail.
* Fix: Logo not showing up when using an SVG.
* Change: Graphs now respect the "Group History" setting.
* API:
* New: Add grouping to graph API commands.
* Other:
* New: Added Google Analytics to collect installation metrics.
* Fix: Reconnecting to the Plex server when server settings are not changed.
## v2.0.19-beta (2018-02-16)
* Monitoring:
* Fix: Connect to Plex Cloud server without keeping it awake.
* Fix: Reconnect to Plex Cloud server after the server wakes up from sleeping.
* Notifications:
* Fix: Don't send Plex Server Up/Down notifications when Tautulli starts up.
* Change: Better handling of Watched notifications.
* UI:
* New: Added Plex server selection dropdown in the settings.
* Fix: Libraries and Users tables not refreshing properly.
* Change: Updated the masked info shown to guests.
* Change: Check for updates without refreshing to the homepage.
* API:
* New: Added update_check to the API.
* Fix: delete_media_info_cache not deleting the cache.
* Change: Document "refresh" parameter for get_library_media_info.
* Other:
* Fix: Show the full changelog since v2 on a fresh install.
## v2.0.18-beta (2018-02-12)
* Notifications:
* Fix: Default text for Tautulli update notifications using the wrong parameter.
* Fix: Playback pause and resume notifications only triggering once.
* Change: Negative operators for custom conditions now use "and" instead of "or".
* UI:
* New: Added button to delete the 3rd party lookup info from the info pages.
* Fix: Missing host info in the login logs when logging in using Firefox.
* Change: Cleaned up settings. Advanced settings are now hidden behind a toggle.
* API:
* New: Updated API documentation for v2.
* Other:
* Fix: DeprecationWarning when using HTTPS with self-signed certificates.
* Change: Deleting the Imgur poster URL also deletes the poster from Imgur (only available for new uploads).
* Change: GitHub repository moved to Tautulli/Tautulli. Old GitHub URLs will still work.
## v2.0.17-beta (2018-02-03)
* Notifications:
* Fix: Unable to use @ mentions tags for Discord and Slack.
* New: Added Zapier notification agent.
* API:
* Fix: get_synced_items returning no results.
* Fix: get_library_media_info returning incorrect media type for photo albums.
* Fix: get_library_media_info not being able to sort by title.
## v2.0.16-beta (2018-01-30)
* Monitoring:
* Fix: Timestamp sometimes showing as "0:60" on the activity cards.
* Fix: Incorrect session information being shown for playback of synced content.
* Fix: Sessions not being stopped when "Playback Stopped" notifications were enabled.
* UI:
* Fix: Stream resolution showing up as "unknown" on the graphs.
* New: Added user filter to the Synced Items table.
* Other:
* New: Option to use the Plex server update channel when checking for updates.
## v2.0.15-beta (2018-01-27)
* Monitoring:
* Fix: Live TV sessions not being stopped in History.
* Fix: Stream location showing as "unknown" on the activity cards.
* New: Improved Live TV details on the activity cards.
* Notifications:
* New: Added labels and collections to notification parameters.
* New: Added more server details to notification parameters.
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
## v2.0.14-beta (2018-01-20)
* Monitoring:
* Change: Added "Cellular" bandwidth to "WAN" in activity header.
* Notifications:
* Fix: Plex Web URL for tracks now go to the album page.
* Fix: Recently added notifications being sent for the entire library when DVR EPG data was refreshed.
* Fix: Notifier settings not loading with an apostrophe in the custom condition values.
* Fix: Custom email addresses not being saved when closing the notifier settings.
* Change: Re-enabled Browser notifications.
* Change: Renamed "PlexPy" update notification parameters to "Tautulli".
* Change: Emails no longer automatically insert HTML line breaks.
* Change: "Date" header added to email notifications.
* UI:
* Change: Show all changelogs since the previous version when updating.
## v2.0.13-beta (2018-01-13)
* Notifications:
* New: Added dropdown selection for email addresses of shared users.
* New: Added more notification options for Join.
* Change: Show "OR" between custom condition values.
* Other:
* New: Use JSON Web Tokens for authentication. Login now works with SSO applications.
* New: Allow the Plex server admin to login as a Tautulli admin using their Plex.tv account.
## v2.0.12-beta (2018-01-07)
* Notifications:
* Fix: Incorrect Plex URL parameter value.
* Change: Custom condition logic is now optional. An implicit "and" is applied between all conditions if the logic is blank.
* UI:
* New: Added separate required LAN/WAN bandwidth in the activity header.
* API:
* Fix: Notify API command not sending notifications.
## v2.0.11-beta (2018-01-05)
* Notifications:
* Fix: Some notification parameters showing up blank.
* UI:
* Fix: Stream data showing up as "None" for pre-v2 history.
* Other:
* Fix: Ability to login using the hashed password.
## v2.0.10-beta (2018-01-04)
* Monitoring:
* Fix: HW transcoding indicator on activity cards incorrect after refreshing.
* Notifications:
* Remove: Notification toggles from library and user settings. Use custom conditions to filter out notifications instead.
* UI:
* Fix: Incorrect examples for some date format options. Also added a few missing date format options. (Thanks @Tommatheussen)
## v2.0.9-beta (2018-01-03)
* Notifications:
* Fix: Notifications failing due to incorrect season/episode number types.
## v2.0.8-beta (2018-01-03)
* Monitoring:
* Fix: Incorrect HW transcoding indicator on activity cards.
* Fix: Long product/player names hidden behind platform icon on activity cards.
* Notifications:
* Fix: Notifications failing due to some missing notification parameters.
## v2.0.7-beta (2018-01-01)
* Monitoring:
* Fix: Incorrect LAN/WAN location on activity cards.
* Fix: Paused time not recording correctly.
* Other:
* Fix: Failed to retrieve synced items when there are special characters in the title.
## v2.0.6-beta (2017-12-31)
* Monitoring:
* New: Beta Plex Cloud support.
* Fix: Update paused time while still paused.
* UI:
* Fix: Stopped time showing as "n/a" on history table.
## v2.0.5-beta (2017-12-31)
* Monitoring:
* Fix: IPv6 addresses overflowing on the activity cards.
* Notifications:
* Fix: Error sending Join notifications.
* UI:
* New: Added total required bandwidth in the activity header.
* Other:
* Fix: Failing to retrieve releases from GitHub.
* Fix: CherryPy SSL connection warning. (Thanks @felixbuenemann)
* Fix: Sanitize script output in logs.
* Change: Login sessions persists across server restarts.
## v2.0.4-beta (2017-12-29)
* Monitoring:
* Fix: Current activity cards duplicating on the homepage.
* Notifications:
* Fix: Concurrent stream notifications being sent when there is an incorrect number of streams.
* UI:
* New: Info pages for collections.
* New: Button to test Plex Web URL override.
* Fix: Library and User pages return to the correct tab when pressing back.
## v2.0.3-beta (2017-12-25)
* Monitoring:
* Fix: Missing sync ID error causing logging to crash.
* Fix: Incorrect optimized version title column name causing logging to crash.
* Notifications:
* Fix: Report correct beta version for Tautulli update notifications.
* UI:
* Fix: Missing CSS for stream info modal.
## v2.0.2-beta (2017-12-24)
* Monitoring:
* Fix: Websocket connection fails to start with existing streams when upgrading to v2.
* Fix: Long request URI for refreshing current activity on the homepage.
* Fix: Missing subtitle database columns.
* Fix: Details for synced and optimized versions reporting incorrectly.
* Notifications:
* Fix: Recently added notifications sending for previously added items. It is now limited to past 24 hours only.
* Fix: Source video/audio/subtitle parameters showing up as blank.
* Change: Validate condition logic when saving a notification agent.
* API:
* Change: API is enabled by default on new installs.
* UI:
* New: Add logo svg files. (Thanks @Fish2)
* New: Updated stream info modal.
* Change: Media info tables sort by sort title instead of title.
* Other:
* Fix: Updating library IDs message on libraries page.
* Fix: Wtched percentage settings not saving after restart.
* Remove: Video Preview Thumbnails setting no longer used.
* Change: Add back HTTP Proxy setting under the Web Interface settings tab.
* Change: "Group Table and Watch Statistics History" and "Current Activity in History Tables" enabled by default on new installs.
## v2.0.1-beta (2017-12-19)
* Monitoring:
* Fix: Missing video_height database column.
* Notifications:
* Fix: Join API key.
* Change: Temporarily disable broken browser notifications.
* UI:
* Fix: Incorrect fallback image for music watch statistics.
## v2.0.0-beta (2017-12-18)
* Monitoring:

View File

@@ -1,48 +1,7 @@
# Contributing to PlexPy
## Issues
In case you read this because you are posting an issue, please take a minute and conside the things below. The issue tracker is not a support forum. It is primarily intended to submit bugs. However, we are glad to help you, and make sure the problem is not caused by PlexPy, but don't expect step-by-step answers.
##### Many issues can simply be solved by:
- Making sure you update to the latest version.
- Turning your device off and on again.
- Analyzing your logs, you just might find the solution yourself!
- Using the **search** function to see if this issue has already been reported/solved.
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
- For basic questions try asking on [Gitter](https://gitter.im/plexpy/general) or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
##### If nothing has worked:
1. Open a new issue on the GitHub [issue tracker](http://github.com/JonnyWong16/plexpy/issues).
2. Provide a clear title to easily help identify your problem.
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
4. Make sure you provide the following information:
- [ ] Version
- [ ] Branch
- [ ] Commit hash
- [ ] Operating system
- [ ] Python version
- [ ] What you did?
- [ ] What happened?
- [ ] What you expected?
- [ ] How can we reproduce your issue?
- [ ] What are your (relevant) settings?
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
## Feature Requests
Feature requests are handled on [FeatHub](http://feathub.com/JonnyWong16/plexpy).
1. Search the existing requests to see if your suggestion has already been submitted.
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
# Contributing to Tautulli
## Pull Requests
If you think you can contribute code to the PlexPy repository, do not hesitate to submit a pull request.
If you think you can contribute code to the Tautulli repository, do not hesitate to submit a pull request.
### Branches
All pull requests should be based on the `dev` branch, to minimize cross merges. When you want to develop a new feature, clone the repository with `git clone origin/dev -b FEATURE_NAME`. Use meaningful commit messages.
@@ -50,12 +9,12 @@ All pull requests should be based on the `dev` branch, to minimize cross merges.
### Python Code
#### Compatibility
The code should work with Python 2.6 and 2.7. Note that PlexPy runs on different platforms, including Network Attached Storage devices such as Synology.
The code should work with Python 2.7. Note that Tautulli runs on different platforms, including Network Attached Storage devices such as Synology.
Re-use existing code. Do not hesitate to add logging in your code. You can the logger module `plexpy.logger.*` for this. Web requests are invoked via `plexpy.request.*` and derived ones. Use these methods to automatically add proper and meaningful error handling.
#### Code conventions
Although PlexPy did not adapt a code convention in the past, we try to follow the [PEP8](http://legacy.python.org/dev/peps/pep-0008/) conventions for future code. A short summary to remind you (copied from http://wiki.ros.org/PyStyleGuide):
Although Tautulli did not adapt a code convention in the past, we try to follow the [PEP8](http://legacy.python.org/dev/peps/pep-0008/) conventions for future code. A short summary to remind you (copied from http://wiki.ros.org/PyStyleGuide):
* 4 space indentation
* 80 characters per line
@@ -71,12 +30,12 @@ Although PlexPy did not adapt a code convention in the past, we try to follow th
Document your code. Use docstrings See [PEP-257](https://www.python.org/dev/peps/pep-0257/) for more information.
#### Continuous Integration
PlexPy has a configuration file for [travis-ci](https://travis-ci.org/). You can add your forked repo to Travis to have it check your code against PEP8, PyLint, and PyFlakes for you. Your pull request will show a green check mark or a red cross on each tested commit, depending on if linting passes.
Tautulli has a configuration file for [travis-ci](https://travis-ci.org/). You can add your forked repo to Travis to have it check your code against PEP8, PyLint, and PyFlakes for you. Your pull request will show a green check mark or a red cross on each tested commit, depending on if linting passes.
### HTML/Template code
#### Compatibility
HTML5 compatible browsers are targetted. There is no specific mobile version of PlexPy yet.
HTML5 compatible browsers are targetted. There is no specific mobile version of Tautulli yet.
#### Conventions
* 4 space indentation

View File

@@ -8,7 +8,7 @@ Reporting Issues:
Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
Feature Requests:
* Feature requests are handled on FeatHub: http://feathub.com/JonnyWong16/plexpy
* Feature requests are handled on FeatHub: http://feathub.com/Tautulli/Tautulli
* Do not post them on the GitHub issues tracker.
-->

242
PlexPy.py
View File

@@ -21,246 +21,8 @@
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
from Tautulli import main
# Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
import argparse
import locale
import signal
import time
import plexpy
from plexpy import config, database, logger, web_socket, webstart
# Register signals, such as CTRL + C
signal.signal(signal.SIGINT, plexpy.sig_handler)
signal.signal(signal.SIGTERM, plexpy.sig_handler)
def main():
"""
Tautulli application entry point. Parses arguments, setups encoding and
initializes the application.
"""
# Fixed paths to Tautulli
if hasattr(sys, 'frozen'):
plexpy.FULL_PATH = os.path.abspath(sys.executable)
else:
plexpy.FULL_PATH = os.path.abspath(__file__)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.ARGS = sys.argv[1:]
# From sickbeard
plexpy.SYS_PLATFORM = sys.platform
plexpy.SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
plexpy.SYS_ENCODING = locale.getpreferredencoding()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
plexpy.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments
parser = argparse.ArgumentParser(
description='A Python based monitoring and tracking tool for Plex Media Server.')
parser.add_argument(
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
parser.add_argument(
'-q', '--quiet', action='store_true', help='Turn off console logging')
parser.add_argument(
'-d', '--daemon', action='store_true', help='Run as a daemon')
parser.add_argument(
'-p', '--port', type=int, help='Force Tautulli to run on a specified port')
parser.add_argument(
'--dev', action='store_true', help='Start Tautulli in the development environment')
parser.add_argument(
'--datadir', help='Specify a directory where to store your data files')
parser.add_argument(
'--config', help='Specify a config file to use')
parser.add_argument(
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
parser.add_argument(
'--nofork', action='store_true', help='Start Tautulli as a service, do not fork when restarting')
args = parser.parse_args()
if args.verbose:
plexpy.VERBOSE = True
if args.quiet:
plexpy.QUIET = True
# Do an intial setup of the logger.
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
verbose=plexpy.VERBOSE)
if args.dev:
plexpy.DEV = True
logger.debug(u"Tautulli is running in the dev environment.")
if args.daemon:
if sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
if args.nofork:
plexpy.NOFORK = True
logger.info("Tautulli is running as a service, it will not fork when restarted.")
if args.pidfile:
plexpy.PIDFILE = str(args.pidfile)
# If the pidfile already exists, plexpy may still be running, so
# exit
if os.path.exists(plexpy.PIDFILE):
try:
with open(plexpy.PIDFILE, 'r') as fp:
pid = int(fp.read())
os.kill(pid, 0)
except IOError as e:
raise SystemExit("Unable to read PID file: %s", e)
except OSError:
logger.warn("PID file '%s' already exists, but PID %d is " \
"not running. Ignoring PID file." %
(plexpy.PIDFILE, pid))
else:
# The pidfile exists and points to a live PID. plexpy may
# still be running, so exit.
raise SystemExit("PID file '%s' already exists. Exiting." %
plexpy.PIDFILE)
# The pidfile is only useful in daemon mode, make sure we can write the
# file properly
if plexpy.DAEMON:
plexpy.CREATEPID = True
try:
with open(plexpy.PIDFILE, 'w') as fp:
fp.write("pid\n")
except IOError as e:
raise SystemExit("Unable to write PID file: %s", e)
else:
logger.warn("Not running in daemon mode. PID file creation " \
"disabled.")
# Determine which data directory and config file to use
if args.datadir:
plexpy.DATA_DIR = args.datadir
else:
plexpy.DATA_DIR = plexpy.PROG_DIR
if args.config:
config_file = args.config
else:
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
# Try to create the DATA_DIR if it doesn't exist
if not os.path.exists(plexpy.DATA_DIR):
try:
os.makedirs(plexpy.DATA_DIR)
except OSError:
raise SystemExit(
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
# Make sure the DATA_DIR is writeable
if not os.access(plexpy.DATA_DIR, os.W_OK):
raise SystemExit(
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
# Put the database in the DATA_DIR
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
if plexpy.DAEMON:
plexpy.daemonize()
# Read config and start logging
plexpy.initialize(config_file)
# Start the background threads
plexpy.start()
# Open connection for websocket
try:
web_socket.start_thread()
except:
logger.warn(u"Websocket :: Unable to open connection.")
plexpy.initialize_scheduler()
# Force the http port if neccessary
if args.port:
http_port = args.port
logger.info('Using forced web server port: %i', http_port)
else:
http_port = int(plexpy.CONFIG.HTTP_PORT)
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if plexpy.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \
"module to enable HTTPS. HTTPS will be disabled.")
plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': http_port,
'http_host': plexpy.CONFIG.HTTP_HOST,
'http_root': plexpy.CONFIG.HTTP_ROOT,
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
'https_cert': plexpy.CONFIG.HTTPS_CERT,
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
webstart.initialize(web_config)
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
plexpy.CONFIG.HTTP_ROOT)
# Wait endlessy for a signal to happen
while True:
if not plexpy.SIGNAL:
try:
time.sleep(1)
except KeyboardInterrupt:
plexpy.SIGNAL = 'shutdown'
else:
logger.info('Received signal: %s', plexpy.SIGNAL)
if plexpy.SIGNAL == 'shutdown':
plexpy.shutdown()
elif plexpy.SIGNAL == 'restart':
plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout':
plexpy.shutdown(restart=True, checkout=True)
else:
plexpy.shutdown(restart=True, update=True)
plexpy.SIGNAL = None
# Call main()
# Call main() from Tautulli.py
if __name__ == "__main__":
main()

View File

@@ -1,8 +1,8 @@
# Tautulli
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/36ggawe)
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://discord.gg/tQcWEUp)
[![Reddit](https://img.shields.io/badge/Reddit-Tautulli-FF5700.svg?style=flat-square)](https://www.reddit.com/r/Tautulli/)
[![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program)
[![Plex Forums](https://img.shields.io/badge/Plex%20Forums-Tautulli-E5A00D.svg?style=flat-square)](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server)
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
@@ -27,56 +27,19 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
## Preview
* [Full preview gallery on our website](http://tautulli.com)
* [Full preview gallery available on our website](http://tautulli.com)
![Tautulli Homepage](http://tautulli.com/images/screenshots/activity.png?v=2)
![Tautulli Homepage](http://tautulli.com/images/screenshots/activity-compressed.jpg?v=2)
## Installation and Support
* [Installation Guides](https://github.com/JonnyWong16/plexpy/wiki/Installation) shows you how to install Tautulli.
* [FAQs](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)) in the wiki can help you with common problems.
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli.
* The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
* Support is available on [Discord](https://discord.gg/tQcWEUp), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server).
**Support** the project by implementing new features, solving support tickets and provide bug fixes.
## Issues & Feature Requests
## Issues
##### Many issues can simply be solved by:
- Making sure you update to the latest version.
- Turning your device off and on again.
- Analyzing your logs, you just might find the solution yourself!
- Using the **search** function to see if this issue has already been reported/solved.
- Checking the [Wiki](https://github.com/JonnyWong16/plexpy/wiki) for
[ [Installation] ](https://github.com/JonnyWong16/plexpy/wiki/Installation) and
[ [FAQs] ](https://github.com/JonnyWong16/plexpy/wiki/Frequently-Asked-Questions-(FAQ)).
- For basic questions try asking on [Discord](https://discord.gg/36ggawe), [Reddit](https://www.reddit.com/r/Tautulli), or the [Plex Forums](https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program) first before opening an issue.
##### If nothing has worked:
1. Open a new issue on the GitHub [issue tracker](http://github.com/JonnyWong16/plexpy/issues).
2. Provide a clear title to easily help identify your problem.
3. Use proper [markdown syntax](https://help.github.com/articles/github-flavored-markdown) to structure your post (i.e. code/log in code blocks).
4. Make sure you provide the following information:
- [ ] Version
- [ ] Branch
- [ ] Commit hash
- [ ] Operating system
- [ ] Python version
- [ ] What you did?
- [ ] What happened?
- [ ] What you expected?
- [ ] How can we reproduce your issue?
- [ ] What are your (relevant) settings?
- [ ] Include a link to your **FULL** (not just a few lines!) log file that has the error. Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
5. Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
## Feature Requests
Feature requests are handled on [FeatHub](http://feathub.com/JonnyWong16/plexpy).
1. Search the existing requests to see if your suggestion has already been submitted.
2. If a similar request exists, give it a thumbs up (+1), or add additional comments to the request.
3. If no similar requests exist, you can create a new one. Make sure to provide a clear title to easily identify the feature request.
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
## License

267
Tautulli.py Executable file
View File

@@ -0,0 +1,267 @@
#!/bin/sh
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
''''which python2.7 >/dev/null 2>&1 && exec python2.7 "$0" "$@" # '''
''''exec echo "Error: Python not found!" # '''
# -*- coding: utf-8 -*-
# This file is part of Tautulli.
#
# Tautulli is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Tautulli is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Tautulli. If not, see <http://www.gnu.org/licenses/>.
import os
import sys
# Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'lib/'))
import argparse
import locale
import signal
import time
import plexpy
from plexpy import config, database, logger, webstart
# Register signals, such as CTRL + C
signal.signal(signal.SIGINT, plexpy.sig_handler)
signal.signal(signal.SIGTERM, plexpy.sig_handler)
def main():
"""
Tautulli application entry point. Parses arguments, setups encoding and
initializes the application.
"""
# Fixed paths to Tautulli
if hasattr(sys, 'frozen'):
plexpy.FULL_PATH = os.path.abspath(sys.executable)
else:
plexpy.FULL_PATH = os.path.abspath(__file__)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.ARGS = sys.argv[1:]
# From sickbeard
plexpy.SYS_PLATFORM = sys.platform
plexpy.SYS_ENCODING = None
try:
locale.setlocale(locale.LC_ALL, "")
plexpy.SYS_LANGUAGE, plexpy.SYS_ENCODING = locale.getdefaultlocale()
except (locale.Error, IOError):
pass
# for OSes that are poorly configured I'll just force UTF-8
if not plexpy.SYS_ENCODING or plexpy.SYS_ENCODING in ('ANSI_X3.4-1968', 'US-ASCII', 'ASCII'):
plexpy.SYS_ENCODING = 'UTF-8'
# Set up and gather command line arguments
parser = argparse.ArgumentParser(
description='A Python based monitoring and tracking tool for Plex Media Server.')
parser.add_argument(
'-v', '--verbose', action='store_true', help='Increase console logging verbosity')
parser.add_argument(
'-q', '--quiet', action='store_true', help='Turn off console logging')
parser.add_argument(
'-d', '--daemon', action='store_true', help='Run as a daemon')
parser.add_argument(
'-p', '--port', type=int, help='Force Tautulli to run on a specified port')
parser.add_argument(
'--dev', action='store_true', help='Start Tautulli in the development environment')
parser.add_argument(
'--datadir', help='Specify a directory where to store your data files')
parser.add_argument(
'--config', help='Specify a config file to use')
parser.add_argument(
'--nolaunch', action='store_true', help='Prevent browser from launching on startup')
parser.add_argument(
'--pidfile', help='Create a pid file (only relevant when running as a daemon)')
parser.add_argument(
'--nofork', action='store_true', help='Start Tautulli as a service, do not fork when restarting')
args = parser.parse_args()
if args.verbose:
plexpy.VERBOSE = True
if args.quiet:
plexpy.QUIET = True
# Do an intial setup of the logger.
logger.initLogger(console=not plexpy.QUIET, log_dir=False,
verbose=plexpy.VERBOSE)
if args.dev:
plexpy.DEV = True
logger.debug(u"Tautulli is running in the dev environment.")
if args.daemon:
if sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
if args.nofork:
plexpy.NOFORK = True
logger.info("Tautulli is running as a service, it will not fork when restarted.")
if args.pidfile:
plexpy.PIDFILE = str(args.pidfile)
# If the pidfile already exists, plexpy may still be running, so
# exit
if os.path.exists(plexpy.PIDFILE):
try:
with open(plexpy.PIDFILE, 'r') as fp:
pid = int(fp.read())
os.kill(pid, 0)
except IOError as e:
raise SystemExit("Unable to read PID file: %s", e)
except OSError:
logger.warn("PID file '%s' already exists, but PID %d is " \
"not running. Ignoring PID file." %
(plexpy.PIDFILE, pid))
else:
# The pidfile exists and points to a live PID. plexpy may
# still be running, so exit.
raise SystemExit("PID file '%s' already exists. Exiting." %
plexpy.PIDFILE)
# The pidfile is only useful in daemon mode, make sure we can write the
# file properly
if plexpy.DAEMON:
plexpy.CREATEPID = True
try:
with open(plexpy.PIDFILE, 'w') as fp:
fp.write("pid\n")
except IOError as e:
raise SystemExit("Unable to write PID file: %s", e)
else:
logger.warn("Not running in daemon mode. PID file creation " \
"disabled.")
# Determine which data directory and config file to use
if args.datadir:
plexpy.DATA_DIR = args.datadir
else:
plexpy.DATA_DIR = plexpy.PROG_DIR
if args.config:
config_file = args.config
else:
config_file = os.path.join(plexpy.DATA_DIR, config.FILENAME)
# Try to create the DATA_DIR if it doesn't exist
if not os.path.exists(plexpy.DATA_DIR):
try:
os.makedirs(plexpy.DATA_DIR)
except OSError:
raise SystemExit(
'Could not create data directory: ' + plexpy.DATA_DIR + '. Exiting....')
# Make sure the DATA_DIR is writeable
if not os.access(plexpy.DATA_DIR, os.W_OK):
raise SystemExit(
'Cannot write to the data directory: ' + plexpy.DATA_DIR + '. Exiting...')
# Put the database in the DATA_DIR
plexpy.DB_FILE = os.path.join(plexpy.DATA_DIR, database.FILENAME)
# Move 'plexpy.db' to 'tautulli.db'
if os.path.isfile(os.path.join(plexpy.DATA_DIR, 'plexpy.db')) and \
not os.path.isfile(os.path.join(plexpy.DATA_DIR, plexpy.DB_FILE)):
try:
os.rename(os.path.join(plexpy.DATA_DIR, 'plexpy.db'), plexpy.DB_FILE)
except OSError as e:
raise SystemExit("Unable to rename plexpy.db to tautulli.db: %s", e)
if plexpy.DAEMON:
plexpy.daemonize()
# Read config and start logging
plexpy.initialize(config_file)
# Start the background threads
plexpy.start()
# Force the http port if neccessary
if args.port:
http_port = args.port
logger.info('Using forced web server port: %i', http_port)
else:
http_port = int(plexpy.CONFIG.HTTP_PORT)
# Check if pyOpenSSL is installed. It is required for certificate generation
# and for CherryPy.
if plexpy.CONFIG.ENABLE_HTTPS:
try:
import OpenSSL
except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \
"module to enable HTTPS. HTTPS will be disabled.")
plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
web_config = {
'http_port': http_port,
'http_host': plexpy.CONFIG.HTTP_HOST,
'http_root': plexpy.CONFIG.HTTP_ROOT,
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT,
'http_proxy': plexpy.CONFIG.HTTP_PROXY,
'enable_https': plexpy.CONFIG.ENABLE_HTTPS,
'https_cert': plexpy.CONFIG.HTTPS_CERT,
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN,
'https_key': plexpy.CONFIG.HTTPS_KEY,
'http_username': plexpy.CONFIG.HTTP_USERNAME,
'http_password': plexpy.CONFIG.HTTP_PASSWORD,
'http_basic_auth': plexpy.CONFIG.HTTP_BASIC_AUTH
}
webstart.initialize(web_config)
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, http_port,
plexpy.CONFIG.HTTP_ROOT)
# Wait endlessy for a signal to happen
while True:
if not plexpy.SIGNAL:
try:
time.sleep(1)
except KeyboardInterrupt:
plexpy.SIGNAL = 'shutdown'
else:
logger.info('Received signal: %s', plexpy.SIGNAL)
if plexpy.SIGNAL == 'shutdown':
plexpy.shutdown()
elif plexpy.SIGNAL == 'restart':
plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout':
plexpy.shutdown(restart=True, checkout=True)
else:
plexpy.shutdown(restart=True, update=True)
plexpy.SIGNAL = None
# Call main()
if __name__ == "__main__":
main()

View File

@@ -2,6 +2,7 @@
import plexpy
from plexpy import version
from plexpy.helpers import anon_url
from plexpy.notifiers import BROWSER_NOTIFIERS
%>
<!doctype html>
@@ -14,34 +15,27 @@
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
${next.headIncludes()}
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.0">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS -->
<!-- Android >M39 icon -->
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0">
<meta name="theme-color" content="#1f1f1f">
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.0" color="#1f1f1f">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="initial-scale=1">
<meta name="format-detection" content="telephone=no">
<!-- IE10 icon -->
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-TileColor" content="#1f1f1f">
<meta name="msapplication-TileImage" content="${http_root}images/favicon/mstile-144x144.png?v=2.0.0">
<meta name="msapplication-config" content="${http_root}xml/IEconfig.xml?v=2.0.0" />
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head>
<body class="content">
@@ -50,16 +44,24 @@
% if _session['user_group'] == 'admin':
% if plexpy.CONFIG.CHECK_GITHUB and not plexpy.CURRENT_VERSION:
<div id="updatebar" style="display: none;">
You're running an unknown version of Tautulli.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
You are running an unknown version of Tautulli.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.COMMITS_BEHIND > 0 and plexpy.INSTALL_TYPE != 'win':
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.common.BRANCH in ('master', 'beta') and plexpy.common.RELEASE != plexpy.LATEST_RELEASE:
<div id="updatebar" style="display: none;">
A <a href="${anon_url('https://github.com/%s/plexpy/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
newer version</a> is available.<br />
You're ${plexpy.COMMITS_BEHIND} commits behind.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Close</a>
A <a href="${anon_url('https://github.com/%s/%s/releases/tag/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.LATEST_RELEASE))}" target="_blank">
new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.COMMITS_BEHIND > 0 and plexpy.CURRENT_VERSION != plexpy.LATEST_VERSION and plexpy.INSTALL_TYPE != 'win':
<div id="updatebar" style="display: none;">
A <a href="${anon_url('https://github.com/%s/%s/compare/%s...%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION, plexpy.LATEST_VERSION))}" target="_blank">
newer version</a> of Tautulli is available!<br />
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
</div>
% else:
<div id="updatebar" style="display: none;"></div>
% endif
% endif
<nav class="navbar navbar-fixed-top">
@@ -71,8 +73,8 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="home">
<img alt="Tautulli" src="${http_root}images/logo-tautulli-50.png" height="40">
<a class="navbar-brand" href="home" title="Tautulli">
<img src="${http_root}images/logo-tautulli-45.png" height="45" alt="PlexPy">
</a>
</div>
<div class="collapse navbar-collapse navbar-right" id="navbar-collapse-1">
@@ -131,7 +133,7 @@
<li><a href="settings"><i class="fa fa-fw fa-cogs"></i> Settings</a></li>
<li role="separator" class="divider"></li>
<li><a href="logs"><i class="fa fa-fw fa-list-alt"></i> View Logs</a></li>
<li><a href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
<li><a href="${anon_url('https://github.com/%s/%s-Wiki/wiki/Frequently-Asked-Questions' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank"><i class="fa fa-fw fa-question-circle"></i> FAQ</a></li>
<li><a href="settings?support=true"><i class="fa fa-fw fa-comment"></i> Support</a></li>
<li role="separator" class="divider"></li>
<li><a href="#" data-target="#donate-modal" data-toggle="modal"><i class="fa fa-fw fa-heart"></i> Donate</a></li>
@@ -145,7 +147,7 @@
<li><a href="#" data-target="#admin-login-modal" data-toggle="modal"><i class="fa fa-fw fa-lock"></i> Admin Login</a></li>
<li role="separator" class="divider"></li>
% endif
% if _session['expiry']:
% if _session['exp']:
<li><a href="${http_root}auth/logout"><i class="fa fa-fw fa-sign-out"></i> Sign Out</a></li>
% endif
</ul>
@@ -168,7 +170,7 @@ ${next.modalIncludes()}
<div id="admin-login-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="admin-login-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<form action="${http_root}auth/login" method="post">
<form id="login-form">
<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">Admin Login</h4>
@@ -197,7 +199,8 @@ ${next.modalIncludes()}
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
<span id="incorrect-login" style="padding-right: 25px; display: none;">Incorrect username or password.</span>
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
</div>
<input type="hidden" id="admin_login" name="admin_login" value="1" />
</form>
@@ -224,15 +227,23 @@ ${next.modalIncludes()}
</div>
</div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
<li><a href="#flattr-donation" role="tab" data-toggle="tab">Flattr</a></li>
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#paypal-donation" role="tab" data-toggle="tab">PayPal</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoin" data-name="Bitcoin" data-address="3FdfJAyNWU15Sf11U9FTgPHuP1hPz32eEN">Bitcoin</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="bitcoincash" data-name="Bitcoin Cash" data-address="1H2atabxAQGaFAWYQEiLkXKSnK9CZZvt2n">Bitcoin Cash</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="ethereum" data-name="Ethereum" data-address="0x77ae4c2b8de1a1ccfa93553db39971da58c873d3">Ethereum</a></li>
<li><a href="#crypto-donation" role="tab" data-toggle="tab" class="crypto-donation" data-coin="litecoin" data-name="Litecoin" data-address="LWpPmUqQYHBhMV83XSCsHzPmKLhJt6r57J">Litecoin</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="paypal-donation" style="text-align: center">
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
<p>
Click the button below to continue to Patreon.
</p>
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
<p>
Click the button below to continue to PayPal.
</p>
@@ -240,14 +251,6 @@ ${next.modalIncludes()}
<img src="images/gold-rect-paypal-34px.png" alt="PayPal">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="flattr-donation" style="text-align: center">
<p>
Click the button below to continue to Flattr.
</p>
<a href="${anon_url('https://flattr.com/submit/auto?user_id=JonnyWong16&url=https://github.com/JonnyWong16/plexpy&title=Tautulli&language=en_GB&tags=github&category=software')}" target="_blank">
<img src="images/flattr-badge-large.png" alt="Flattr">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="crypto-donation">
<label>QR Code</label>
<pre id="crypto_qr_code" style="text-align: center"></pre>
@@ -289,20 +292,53 @@ ${next.modalIncludes()}
<script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
% if _session['user_group'] == 'admin' and plexpy.CONFIG.BROWSER_ENABLED:
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script>
% else:
% endif
<script>
% if _session['user_group'] == 'admin':
$('#updateDismiss').click(function() {
$('#updatebar').slideUp('slow');
$('body').on('click', '#updateDismiss', function() {
$('#updatebar').fadeOut();
// Set cookie to remember dismiss decision for 1 hour.
setCookie('updateDismiss', 'true', 1/24);
});
if (!getCookie('updateDismiss')) {
$('#updatebar').show();
if ($('#updatebar').html().length > 0) {
$('#updatebar').show();
}
}
function checkUpdate(_callback) {
// Allow the update bar to show again if previously dismissed.
setCookie('updateDismiss', 'true', 0);
$.ajax({
url: 'update_check',
complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = '';
if (result.update === null) {
msg = 'You are running an unknown version of Tautulli.<br />' +
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn();
} else if (result.update === true && result.release === true) {
msg = 'A <a href="' + result.release_url + '" target="_blank">new release (' + result.latest_release + ')</a> of Tautulli is available!<br />' +
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn();
} else if (result.update === true && result.release === false) {
msg = 'A <a href="' + result.compare_url + '" target="_blank">newer version</a> of Tautulli is available!<br />' +
'You are '+ result.commits_behind + ' commit' + (result.commits_behind > 1 ? 's' : '') + ' behind.<br />' +
'<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
$('#updatebar').html(msg).fadeIn();
} else if (result.update === false) {
showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
}
if (_callback) {
_callback();
}
}
});
}
$("#nav-shutdown").click(function() {
@@ -321,11 +357,9 @@ ${next.modalIncludes()}
});
});
$("#nav-update").first().one("click", function () {
// Allow the update bar to show again if previously dismissed.
setCookie('updateDismiss', 'true', 0);
$(this).html('<i class="fa fa-spin fa-refresh"></i> Checking');
window.location.href = "checkGithub";
$('#nav-update').click(function () {
$(this).html('<i class="fa fa-fw fa-spin fa-refresh"></i> Checking');
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-circle-up"></i> Check for Updates'); });
});
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
@@ -397,6 +431,29 @@ ${next.modalIncludes()}
$('#admin-login-modal').on('shown.bs.modal', function () {
$('#admin-login-modal #username').focus()
})
$('#login-form').submit(function(event) {
event.preventDefault();
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In');
$.ajax({
url: '${http_root}auth/signin',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
statusCode: {
200: function() {
window.location = "${http_root}";
},
401: function() {
$('#incorrect-login').show();
$('#username').focus();
}
},
complete: function() {
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i>&nbsp; Sign In');
}
});
});
% endif
</script>
${next.javascriptIncludes()}

View File

@@ -22,11 +22,11 @@ DOCUMENTATION :: END
% if plexpy.CURRENT_VERSION:
<tr>
<td>Git Branch:</td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CONFIG.GIT_BRANCH}</a></td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/tree/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CONFIG.GIT_BRANCH}</a></td>
</tr>
<tr>
<td>Git Commit Hash:</td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_BRANCH))}">${plexpy.CURRENT_VERSION}</a></td>
<td><a class="no-highlight" href="${anon_url('https://github.com/%s/%s/commit/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO, plexpy.CURRENT_VERSION))}">${plexpy.CURRENT_VERSION}</a></td>
</tr>
% endif
<tr>
@@ -49,6 +49,10 @@ DOCUMENTATION :: END
<td>Cache Directory:</td>
<td>${plexpy.CONFIG.CACHE_DIR}</td>
</tr>
<tr>
<td>Newsletter Directory:</td>
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
</tr>
<tr>
<td>GeoLite2 Database:</td>
% if plexpy.CONFIG.GEOIP_DB:
@@ -74,19 +78,19 @@ DOCUMENTATION :: END
<tr>
<td class="top-line">Resources:</td>
<td class="top-line">
<a id="source-link" class="no-highlight" href="${anon_url('https://github.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" target="_blank">GitHub Source</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/plexpy/issues' % plexpy.CONFIG.GIT_USER)}" data-id="issue">GitHub Issues</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/plexpy' % plexpy.CONFIG.GIT_USER)}" data-id="feature request">FeatHub Feature Requests</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki' % plexpy.CONFIG.GIT_USER)}" target="_blank">Tautulli Wiki</a> |
<a id="faq-source-link" class="no-highlight" href="${anon_url('https://github.com/%s/plexpy/wiki/Frequently-Asked-Questions-(FAQ)' % plexpy.CONFIG.GIT_USER)}" target="_blank">Tautulli FAQ</a>
<a class="no-highlight" href="${anon_url('http://tautulli.com')}" target="_blank">Tautulli Website</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Source</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('https://github.com/%s/%s-Issues' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="issue">GitHub Issues</a> |
<a class="no-highlight" href="${anon_url('https://github.com/%s/%s-Wiki' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" target="_blank">GitHub Wiki</a> |
<a class="no-highlight guidelines-modal-link" href="${anon_url('http://feathub.com/%s/%s' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}" data-id="feature request">FeatHub Feature Requests</a>
</td>
</tr>
<tr>
<td>Support:</td>
<td>
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/36ggawe')}" target="_blank">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://discord.gg/tQcWEUp')}" target="_blank">Tautulli Discord Server</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://www.reddit.com/r/Tautulli')}" target="_blank">Tautulli Subreddit</a> |
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/169591/plexpy-another-plex-monitoring-program')}" target="_blank">Plex Forums</a>
<a class="no-highlight support-modal-link" href="${anon_url('https://forums.plex.tv/discussion/307821/tautulli-monitor-your-plex-media-server')}" target="_blank">Plex Forums</a>
</td>
</tr>
</tbody>
@@ -112,7 +116,6 @@ DOCUMENTATION :: END
$('.guidelines-modal-link').on('click', function (e) {
e.preventDefault();
$('#guidelines-link').attr('href', $('#source-link').attr('href'));
$('#guidelines-type').text($(this).data('id'))
$('#guidelines-modal').modal();
$('#guidelines-continue').attr('href', $(this).attr('href')).on('click', function () {
@@ -121,7 +124,6 @@ DOCUMENTATION :: END
});
$('.support-modal-link').on('click', function (e) {
e.preventDefault();
$('#faq-link').attr('href', $('#faq-source-link').attr('href'));
$('#support-modal').modal();
$('#support-continue').attr('href', $(this).attr('href')).on('click', function () {
$('#support-modal').modal('hide');

View File

@@ -359,11 +359,13 @@ table.display tr.shown + tr:hover {
}
table.display tr.shown + tr:hover a,
table.display tr.shown + tr td:hover a,
table.display tr.shown + tr td:hover a .fa,
table.display tr.shown + tr .pagination > .active > a,
table.display tr.shown + tr .pagination > .active > a:hover {
color: #fff;
}
table.display tr.shown + tr table[id^='history_child'] td:hover a,
table.display tr.shown + tr table[id^='history_child'] td:hover a .fa,
table.display tr.shown + tr table[id^='media_info_child'] > tr > td:hover a,
table.display tr.shown + tr table[id^='media_info_child'] tr.shown + tr table[id^='media_info_child'] td:hover a {
color: #cc7b19;

View File

@@ -64,43 +64,54 @@ DOCUMENTATION :: END
from collections import defaultdict
from urllib import quote
from plexpy import helpers
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES
import plexpy
%>
<% data = defaultdict(lambda: 'Unknown', **session) %>
<% sk = data['session_key'] %>
<%
data = defaultdict(lambda: 'Unknown', **session)
sk = data['session_key']
href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#'
parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#'
grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#'
user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#'
%>
<div class="dashboard-activity-instance" id="activity-instance-${sk}" data-key="${sk}" data-id="${data['session_id']}"
data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}">
<div class="dashboard-activity-container">
<div class="dashboard-activity-background-overlay">
% if data['channel_stream'] == '0':
<div id="background-${sk}" class="dashboard-activity-background" style="background-image: url(pms_image_proxy?img=${data['art']}&width=500&height=280&fallback=art&refresh=true);"></div>
% else:
% if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')):
<div id="background-${sk}" class="dashboard-activity-background" style="background-image: url(${data['art']});"></div>
% else:
<!--Hacky solution to escape the image url until I come up with something better-->
<div id="background-${sk}" class="dashboard-activity-background" style="background-image: url(pms_image_proxy?img=${quote(data['art'] or data['thumb'])}&width=500&height=280&fallback=art&refresh=true&clip=true);"></div>
% endif
% endif
<%
if data['channel_stream'] == 0:
background_url = 'pms_image_proxy?img=' + data['art'] + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true'
else:
if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')):
background_url = data['art']
else:
background_url = 'pms_image_proxy?img=' + quote(data['art'] or data['thumb']) + '&width=500&height=280&fallback=art&refresh=true&clip=true'
%>
<div id="background-${sk}" class="dashboard-activity-background" style="background-image: url(${background_url});">
<div class="dashboard-activity-poster-container hidden-xs">
% if data['media_type'] == 'track':
<div id="poster-${sk}-bg" class="dashboard-activity-poster-blur" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
<div id="poster-${sk}-bg" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover&refresh=true);"></div>
% endif
% if data['channel_stream'] == '0':
% if data['channel_stream'] == 0:
% if data['media_type'] == 'movie':
<a id="poster-url-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
</a>
% elif data['media_type'] == 'episode':
<a id="poster-url-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">
<a id="poster-url-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['grandparent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
</a>
% elif data['media_type'] == 'track':
<a id="poster-url-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}">
<a id="poster-url-${sk}" href="${parent_href}" title="${data['parent_title']}">
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=300&fallback=cover&refresh=true);"></div>
</a>
% elif data['media_type'] in ('photo', 'clip'):
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
% if data['extra_type']:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['art'].replace('/art', '/thumb') or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
% else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['parent_thumb'] or data['thumb']}&width=300&height=450&fallback=poster&refresh=true);"></div>
% endif
% else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/art.png);"></div>
% endif
@@ -109,7 +120,7 @@ DOCUMENTATION :: END
<div id="poster-${sk}" class="dashboard-activity-poster-blur" style="background-image: url(${data['channel_icon']});"></div>
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(${data['channel_icon']});"></div>
% else:
<div id="poster-${sk}" class="dashboard-activity-poster-blur" style="background-image: url(pms_image_proxy?img=${data['channel_icon']}&width=300&height=300&fallback=cover&refresh=true);"></div>
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(pms_image_proxy?img=${data['channel_icon']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover&refresh=true);"></div>
<div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(pms_image_proxy?img=${data['channel_icon']}&width=300&height=300&fallback=cover&refresh=true);"></div>
% endif
% endif
@@ -134,15 +145,15 @@ DOCUMENTATION :: END
<ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item">
<div class="sub-heading">Product</div>
<div class="sub-value">${data['product']}</div>
<div class="sub-value platform-right">${data['product']}</div>
</li>
<li class="dashboard-activity-info-item">
<div class="sub-heading">Player</div>
<div class="sub-value">${data['player']}</div>
<div class="sub-value platform-right">${data['player']}</div>
</li>
<li class="dashboard-activity-info-item">
<div class="sub-heading">Quality</div>
<div class="sub-value" id="stream_quality-${sk}">
<div class="sub-value platform-right" id="stream_quality-${sk}">
% if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown':
<%
br = helpers.cast_to_int(data['stream_bitrate']) or ''
@@ -158,11 +169,19 @@ DOCUMENTATION :: END
% endif
</div>
</li>
% if data['optimized_version'] == '1':
% if data['optimized_version'] == 1:
<li class="dashboard-activity-info-item">
<div class="sub-heading">Optimized</div>
<div class="sub-value" id="optimized_version-${sk}">
${data['optimized_version_profile']}
${data['optimized_version_profile']} (${data['optimized_version_title']})
</div>
</li>
% endif
% if data['synced_version'] == 1:
<li class="dashboard-activity-info-item">
<div class="sub-heading">Synced</div>
<div class="sub-value" id="synced_version-${sk}">
${data['synced_version_profile']}
</div>
</li>
% endif
@@ -170,14 +189,14 @@ DOCUMENTATION :: END
<ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item">
% if _session['user_group'] == 'admin':
<div class="sub-heading"><span class="raw-stream-info-modal" data-toggle="modal" data-target="#raw-stream-info-modal" data-key="${sk}">Stream</span></div>\
<div class="sub-heading"><span class="raw-stream-info-modal" data-toggle="modal" data-target="#raw-stream-info-modal" data-key="${sk}">Stream</span></div>
% else:
<div class="sub-heading">Stream</div>
% endif
<div class="sub-value" id="transcode_decision-${sk}">
% if data['transcode_decision'] == 'transcode':
Transcode
% if data['transcode_throttled'] == '1':
% if data['transcode_throttled'] == 1:
(Throttled)
% else:
(Speed: ${data['transcode_speed']})
@@ -185,15 +204,15 @@ DOCUMENTATION :: END
% elif data['transcode_decision'] == 'copy':
Direct Stream
% else:
Direct Play ${'(Synced)' if data['synced_version'] == '1' else ''}
Direct Play
% endif
</div>
</li>
<li class="dashboard-activity-info-item">
<div class="sub-heading">Container</div>
<div class="sub-value" id="transcode_container-${sk}">
% if data.get('stream_container_decision') == 'transcode':
Transcode (${data['container'].upper()} &rarr; ${data['stream_container'].upper()})
% if data['stream_container_decision'] == 'transcode':
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
% else:
Direct Play (${data['container'].upper()})
% endif
@@ -204,19 +223,16 @@ DOCUMENTATION :: END
<div class="sub-heading">Video</div>
<div class="sub-value" id="video_decision-${sk}">
% if data['media_type'] in ('movie', 'episode', 'clip'):
% if data.get('stream_video_decision') == 'transcode':
% if data['stream_video_decision'] == 'transcode':
<%
hw_d = hw_e = ''
if data['transcode_hw_requested'] == '1' and data['transcode_hw_full_pipeline'] == '0':
hw_d = ' (HW)'
elif data['transcode_hw_requested'] == '1' and data['transcode_hw_full_pipeline'] == '1':
hw_d = hw_e = ' (HW)'
hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
%>
Transcode (${data['video_codec'].upper()}${hw_d} ${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} &rarr; ${data['stream_video_codec'].upper()}${hw_e} ${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% elif data.get('stream_video_decision') == 'copy':
Direct Stream (${data['stream_video_codec'].upper()} ${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
Transcode (${data['video_codec'].upper()}${hw_d} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% elif data['stream_video_decision'] == 'copy':
Direct Stream (${data['stream_video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])})
% else:
Direct Play (${data['video_codec'].upper()} ${plexpy.common.VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])})
% endif
% elif data['media_type'] == 'photo':
Direct Play (${data['width']}x${data['height']})
@@ -228,12 +244,12 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item">
<div class="sub-heading">Audio</div>
<div class="sub-value" id="audio_decision-${sk}">
% if data.get('stream_audio_decision') == 'transcode':
Transcode (${plexpy.common.AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} &rarr; ${plexpy.common.AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% elif data.get('stream_audio_decision') == 'copy':
Direct Stream (${plexpy.common.AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% if data['stream_audio_decision'] == 'transcode':
Transcode (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()} <i class="fa fa-long-arrow-right"></i> ${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% elif data['stream_audio_decision'] == 'copy':
Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% else:
Direct Play (${plexpy.common.AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['audio_codec'], data['audio_codec'].upper())} ${data['audio_channel_layout'].split('(')[0].capitalize()})
% endif
</div>
</li>
@@ -242,15 +258,15 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item">
<div class="sub-heading">Subtitle</div>
<div class="sub-value" id="subtitle_decision-${sk}">
% if data['subtitles'] == '1':
% if data['subtitles'] == 1:
% if data['stream_subtitle_decision'] == 'transcode':
Transcode (${data['subtitle_codec'].upper()} &rarr; ${data['stream_subtitle_codec'].upper()})
Transcode (${data['subtitle_codec'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()})
% elif data['stream_subtitle_decision'] == 'copy':
Direct Stream (${data['subtitle_codec'].upper()})
% elif data['stream_subtitle_decision'] == 'burn':
Burn (${data['subtitle_codec'].upper()})
% else:
Direct Play (${data['subtitle_codec'].upper()})
Direct Play (${data['stream_subtitle_codec'].upper() if data['synced_version'] else data['subtitle_codec'].upper()})
% endif
% else:
None
@@ -262,19 +278,24 @@ DOCUMENTATION :: END
<ul class="list-unstyled dashboard-activity-info-list">
<li class="dashboard-activity-info-item">
<div class="sub-heading">Location</div>
<div class="sub-value">
<div class="sub-value time-right">
<span id="location-${sk}">${data['location'].upper()}</span>:
% if data['ip_address'] != 'N/A':
${'LAN' if data['local'] == '1' else 'WAN'}: ${data['ip_address']}
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
</a>
<script>
isPrivateIP("${data['ip_address']}").then(function () {
$("#external_ip-${sk}").hide();
}, function () {
$("#external_ip-${sk}").show();
});
</script>
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
% if data['relay']:
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
% else:
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
<span id="external_ip-${sk}" class="external-ip-tooltip" data-toggle="tooltip" title="Lookup External IP" style="display: none;"><i class="fa fa-map-marker"></i></span>
</a>
<script>
isPrivateIP("${data['ip_address']}").then(function () {
$("#external_ip-${sk}").hide();
}, function () {
$("#external_ip-${sk}").show();
});
</script>
% endif
% else:
N/A
% endif
@@ -282,19 +303,18 @@ DOCUMENTATION :: END
</li>
<li class="dashboard-activity-info-item">
<div class="sub-heading">Bandwidth</div>
<div class="sub-value">
% if data['media_type'] != 'photo' and helpers.cast_to_int(data['bandwidth']):
<div class="sub-value time-right">
% if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown':
<%
bw = helpers.cast_to_int(data['bandwidth'])
if bw != "Unknown":
if bw > 1000:
bw = str(round(bw / 1000.0, 1)) + ' Mbps'
else:
bw = str(bw) + ' kbps'
if bw > 1000:
bw = str(round(bw / 1000.0, 1)) + ' Mbps'
else:
bw = str(bw) + ' kbps'
%>
<span id="stream-bandwidth-${sk}">${bw}</span>
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate"><i class="fa fa-info-circle"></i></span>
% elif data['synced_version'] == '1' or data['channel_stream'] == '1':
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
% elif data['synced_version'] == 1 or data['channel_stream'] == 1:
<span id="stream-bandwidth-${sk}">None</span>
% else:
<span id="stream-bandwidth-${sk}">Unknown</span>
@@ -306,7 +326,9 @@ DOCUMENTATION :: END
</div>
% if data['media_type'] != 'photo':
<div class="dashboard-activity-info-time">
% if data['view_offset']:
% if data['live'] == 1:
<br />Live
% elif data['view_offset']:
ETA:
<span id="stream-eta-${sk}">
<script>
@@ -334,19 +356,19 @@ DOCUMENTATION :: END
</div>
<div class="dashboard-activity-progress">
<div class="dashboard-activity-progress-bar">
% if data['live'] == 1:
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-toggle="tooltip" title="Stream Progress Live">Live</div>
% else:
<div id="buffer-bar-${sk}" class="buffer-bar" style="width: ${data['transcode_progress']}%" data-toggle="tooltip" title="Transcoder Progress ${data['transcode_progress']}%">${data['transcode_progress']}%</div>
<div id="progress-bar-${sk}" class="progress-bar" style="width: ${data['progress_percent']}%" data-last_view_offset="${data['view_offset']}" data-view_offset="${data['view_offset']}" data-stream_duration="${data['stream_duration']}" data-state="${data['state']}" data-toggle="tooltip" title="Stream Progress ${data['progress_percent']}%">${data['progress_percent']}%</div>
% endif
</div>
</div>
</div>
<div class="dashboard-activity-metadata-wrapper">
% if data['user_id']:
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">
<a href="${user_href}" title="${data['friendly_name']}">
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
</a>
% else:
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
% endif
<div class="dashboard-activity-metadata-title-container">
<div id="play-state-${sk}" class="dashboard-activity-metadata-play_state-icon" title="${data['state'].capitalize()}">
% if data['state'] == 'playing':
@@ -358,22 +380,22 @@ DOCUMENTATION :: END
% endif
</div>
<div class="dashboard-activity-metadata-title">
% if data['channel_stream'] == '0':
% if data['media_type'] == 'movie':
<a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'episode':
<a href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
- <a href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'track':
<a id="metadata-grandparent_title-${sk}" href="info?rating_key=${data['grandparent_rating_key']}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
- <a id="metadata-title-${sk}" href="info?rating_key=${data['rating_key']}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'photo':
<span title="${data['parent_title']}">${data['parent_title']}</span>
% elif data['media_type'] == 'clip':
<span title="${data['title']}">${data['title']}</span>
% else:
<span title="${data['title']}">${data['title']}</span>
% endif
% if data['channel_stream'] == 0:
% if data['media_type'] == 'movie':
<a href="${href}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'episode':
<a href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
- <a href="${href}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'track':
<a id="metadata-grandparent_title-${sk}" href="${grandparent_href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
- <a id="metadata-title-${sk}" href="${href}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'photo':
<span title="${data['parent_title']}">${data['parent_title']}</span>
% elif data['media_type'] == 'clip':
<span title="${data['title']}">${data['title']}</span>
% else:
<span title="${data['title']}">${data['title']}</span>
% endif
% elif data['media_type'] == 'episode' and data['grandparent_title']:
<span title="${data['grandparent_title']}">${data['grandparent_title']}</span>
- <span title="${data['title']}">${data['title']}</span>
@@ -383,7 +405,11 @@ DOCUMENTATION :: END
</div>
</div>
<div class="dashboard-activity-metadata-subtitle-container">
% if data['channel_stream'] == '0':
% if data['live'] == 1:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Plex Live TV">
<i class="fa fa-fw fa-television"></i>&nbsp;
</div>
% elif data['channel_stream'] == 0:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
% if data['media_type'] == 'movie':
<i class="fa fa-fw fa-film"></i>&nbsp;
@@ -398,23 +424,30 @@ DOCUMENTATION :: END
% endif
</div>
% else:
<div id="media-type-${sk}" title="Channel">
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Channel">
<i class="fa fa-fw fa-cloud"></i>&nbsp;
</div>
% endif
<div class="dashboard-activity-metadata-subtitle">
% if data['channel_stream'] == '0':
% if data['live'] == 1:
<span title="Plex Live TV" class="sub-heading">Plex Live TV</span>
% elif data['channel_stream'] == 0:
% if data['media_type'] == 'movie':
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
% elif data['media_type'] == 'episode':
<a href="info?rating_key=${data['parent_rating_key']}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
&middot; <a href="info?rating_key=${data['rating_key']}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
<a href="${parent_href}" title="Season ${data['parent_media_index']}" class="sub-heading">S${data['parent_media_index']}</a>
&middot; <a href="${href}" title="Episode ${data['media_index']}" class="sub-heading">E${data['media_index']}</a>
% elif data['media_type'] == 'track':
<a id="metadata-parent_title-${sk}" href="info?rating_key=${data['parent_rating_key']}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
<a id="metadata-parent_title-${sk}" href="${parent_href}" title="${data['parent_title']}" class="sub-heading">${data['parent_title']}</a>
% elif data['media_type'] == 'photo':
<span title="${data['title']}" class="sub-heading">${data['title']}</span>
% else:
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
% if data['extra_type']:
<% extra_type = EXTRA_TYPES.get(data['extra_type'], data['sub_type'].capitalize()) %>
<span title="${data['year']} (${extra_type})" class="sub-heading">${data['year']} (${extra_type})</span>
% else:
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
% endif
% endif
% elif data['channel_title']:
<span title="${data['channel_title']}" class="sub-heading">${data['channel_title']}</span>
@@ -435,11 +468,7 @@ DOCUMENTATION :: END
% endif
</div>
<div class="dashboard-activity-metadata-user">
% if data['user_id']:
<a href="user?user_id=${data['user_id']}" title="${data['friendly_name']}">${data['friendly_name']}</a>
% else:
${data['friendly_name']}
% endif
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a>
</div>
</div>
</div>

View File

@@ -47,24 +47,12 @@ DOCUMENTATION :: END
</div>
<p class="help-block">Change the library's picture in Tautulli. To reset to default, leave this field empty and save.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${helpers.checked(data['do_notify'])}> Enable notifications
</label>
<p class="help-block">Uncheck this if you do not want to receive notifications for this library's activity.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history
</label>
<p class="help-block">Uncheck this if you do not want to keep any history on this library's activity.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="do_notify_created" name="do_notify_created" value="1" ${helpers.checked(data['do_notify_created'])}> Enable recently added notifications
</label>
<p class="help-block">Uncheck this if you do not want to receive recently added notifications for this library.</p>
</div>
% if data['section_id']:
<div class="form-group">
<button class="btn btn-danger" id="delete-all-history">Purge</button>
@@ -85,15 +73,7 @@ DOCUMENTATION :: END
// Save library options
$("#save_library").on('click', function () {
var custom_thumb = $("#custom_thumb_url").val();
var do_notify = 0;
var do_notify_created = 0;
var keep_history = 0;
if ($("#do_notify").is(":checked")) {
do_notify = 1;
}
if ($("#do_notify_created").is(":checked")) {
do_notify_created = 1;
}
if ($("#keep_history").is(":checked")) {
keep_history = 1;
}
@@ -103,8 +83,6 @@ DOCUMENTATION :: END
data: {
section_id: '${data["section_id"]}',
custom_thumb: custom_thumb,
do_notify: do_notify,
do_notify_created: do_notify_created,
keep_history: keep_history
},
cache: false,

View File

@@ -56,12 +56,6 @@ DOCUMENTATION :: END
</div>
<p class="help-block">Change the users profile picture in Tautulli. To reset to default, leave this field empty and save.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="do_notify" name="do_notify" value="1" ${helpers.checked(data['do_notify'])}> Enable notifications
</label>
<p class="help-block">Uncheck this if you do not want to receive notifications for this user's activity.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="keep_history" name="keep_history" value="1" ${helpers.checked(data['keep_history'])}> Keep history
@@ -95,12 +89,8 @@ DOCUMENTATION :: END
$("#save_user").on('click', function () {
var friendly_name = $("input#friendly_name").val();
var custom_thumb = $("#custom_avatar_url").val();
var do_notify = 0;
var keep_history = 0;
var allow_guest = 0;
if ($("#do_notify").is(":checked")) {
do_notify = 1;
}
if ($("#keep_history").is(":checked")) {
keep_history = 1;
}
@@ -114,7 +104,6 @@ DOCUMENTATION :: END
user_id: '${data["user_id"]}',
friendly_name: friendly_name,
custom_thumb: custom_thumb,
do_notify: do_notify,
keep_history: keep_history,
allow_guest: allow_guest
},

View File

@@ -2,7 +2,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">
@@ -11,7 +11,7 @@
<div class="header-bar">
<span><i class="fa fa-bar-chart"></i> Graphs</span>
</div>
<div class="button-bar hidden-xs">
<div class="button-bar">
<div class="btn-group" id="user-selection">
<label>
<select name="graph-user" id="graph-user" class="btn" style="color: inherit;">
@@ -39,12 +39,12 @@
</div>
<div class="input-group pull-right" style="width: 1px;" id="days-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" data-default="7" data-toggle="tooltip" title="Min: 1 day" />
<input type="number" class="form-control number-input" name="graph-days" id="graph-days" value="${config['graph_days']}" min="1" data-default="7" data-toggle="tooltip" title="Min: 1 day" />
<span class="input-group-addon btn-dark inactive">days</span>
</div>
<div class="input-group pull-right" style="width: 1px;" id="months-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control" name="graph-months" id="graph-months" value="${config['graph_months']}" min="1" data-default="12" data-toggle="tooltip" title="Min: 1 month" />
<input type="number" class="form-control number-input" name="graph-months" id="graph-months" value="${config['graph_months']}" min="1" data-default="12" data-toggle="tooltip" title="Min: 1 month" />
<span class="input-group-addon btn-dark inactive">months</span>
</div>
</div>

View File

@@ -3,7 +3,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">
@@ -113,8 +113,8 @@
// Load user ids and names (for the selector)
$.ajax({
url: 'get_user_names',
type: 'get',
dataType: "json",
type: 'GET',
dataType: 'json',
success: function (data) {
var select = $('#history-user');
data.sort(function (a, b) {
@@ -130,7 +130,7 @@
function loadHistoryTable(media_type, selected_user_id) {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
type: 'POST',
data: function (d) {
return {
json_data: JSON.stringify(d),
@@ -138,9 +138,13 @@
user_id: selected_user_id
};
}
}
};
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] });
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);
@@ -160,7 +164,7 @@
}
var media_type = null;
var selected_user_id = "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
var selected_user_id = "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}";
loadHistoryTable(media_type, selected_user_id);
% if _session['user_group'] == 'admin':

View File

@@ -5,7 +5,15 @@
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title" id="myModalLabel">
<strong><span id="modal_header_ip_address">
% if data.get('media_type'):
<% h = {'episode': 'TV Show', 'track': 'Music'} %>
<i class="fa fa-history"></i> ${h.get(data['media_type'], data['media_type'].title())} History for <span id="date-header">${data['start_date']}</span>
% elif data.get('transcode_decision'):
<% h = {'copy': 'Direct Stream'} %>
<i class="fa fa-history"></i> ${h.get(data['transcode_decision'], data['transcode_decision'].title())} History for <span id="date-header">${data['start_date']}</span>
% else:
<i class="fa fa-history"></i> History for <span id="date-header">${data['start_date']}</span>
% endif
</span></strong>
</h4>
</div>
@@ -13,11 +21,18 @@
<table class="display history_table" id="history_table_modal" width="100%">
<thead>
<tr>
<th align="left" id="started">Started</th>
<th align="left" id="stopped">Stopped</th>
<th align="left" id="delete_row">Delete</th>
<th align="left" id="date">Date</th>
<th align="left" id="friendly_name">User</th>
<th align="left" id="player">Player</th>
<th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th>
<th align="left" id="device">Player</th>
<th align="left" id="title">Title</th>
<th align="left" id="started">Started</th>
<th align="left" id="paused_counter">Paused</th>
<th align="left" id="stopped">Stopped</th>
<th align="left" id="duration">Duration</th>
<th align="left" id="percent_complete"></th>
</tr>
</thead>
<tbody>
@@ -28,28 +43,31 @@
</div>
</div>
<script src="${http_root}js/tables/history_table_modal.js${cache_param}"></script>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script>
$(document).ready(function() {
$('#date-header').html(moment('${data["start_date"]}','YYYY-MM-DD').format('ddd MMM Do YYYY'));
history_table_modal_options.ajax = {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify(d),
grouping: false,
user_id: "${data['user_id']}",
start_date: "${data['start_date']}",
media_type: "${data.get('media_type')}",
transcode_decision: "${data.get('transcode_decision')}"
};
}
}
};
history_table = $('#history_table_modal').DataTable(history_table_options);
history_table.columns([0, 3, 4, 8, 10, 11]).visible(false);
history_table = $('#history_table_modal').DataTable(history_table_modal_options);
clearSearchButton('history_table_modal', history_table);
$('#history-modal').on('shown.bs.modal', function() {
history_table.columns.adjust().draw();
});
});
</script>
% else:

View File

@@ -71,34 +71,35 @@ DOCUMENTATION :: END
%>
<div class="dashboard-stats-instance" id="stats-instance-${stat_id}" data-stat_id="${stat_id}">
<div class="dashboard-stats-container">
<div class="dashboard-stats-background-overlay">
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
% if row0['art']:
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(pms_image_proxy?img=${row0['art']}&width=500&height=280&fallback=art);"></div>
% else:
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(images/art.png);"></div>
% endif
% elif stat_id == 'top_platforms':
<div id="stats-background-${stat_id}" class="dashboard-stats-background platform-${row0['platform_name']} no-image"></div>
% else:
<div id="stats-background-${stat_id}" class="dashboard-stats-background flat"></div>
% endif
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
% if row0['art']:
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(pms_image_proxy?img=${row0['art']}&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art);">
% else:
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(images/art.png);">
% endif
% elif stat_id == 'top_platforms':
<div id="stats-background-${stat_id}" class="dashboard-stats-background platform-${row0['platform_name']}-rgba no-image">
% else:
<div id="stats-background-${stat_id}" class="dashboard-stats-background flat">
% endif
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
<div class="dashboard-stats-poster-container hidden-xs">
% if stat_id in ('top_music', 'popular_music'):
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster-blur" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=${type});"></div>
<div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&opacity=60&background=282828&blur=3&fallback=cover);"></div>
% endif
<a id="stats-thumb-url-${stat_id}" href="info?rating_key=${row0['rating_key']}" title="${row0['title']}">
<% type = 'cover' if stat_id in ('top_music', 'popular_music') else 'poster' %>
<% height, type = ('300', 'cover') if stat_id in ('top_music', 'popular_music') else ('450', 'poster') %>
<% href = 'info?rating_key={}'.format(row0['rating_key']) if row0['rating_key'] else '#' %>
<a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}">
% if row0['thumb']:
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=300&fallback=${type});"></div>
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(pms_image_proxy?img=${row0['thumb']}&width=300&height=${height}&fallback=${type});"></div>
% else:
<div id="stats-thumb-${stat_id}" class="dashboard-stats-${type}" style="background-image: url(images/${type}.png);"></div>
% endif
</a>
</div>
% elif stat_id == 'top_users':
<a id="stats-thumb-url-${stat_id}" href="user?user_id=${row0['user_id']}" title="${row0['friendly_name']}" class="hidden-xs">
<% user_href = 'user?user_id={}'.format(row0['user_id']) if row0['user_id'] else '#' %>
<a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['friendly_name']}" class="hidden-xs">
<div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
</a>
% elif stat_id == 'top_platforms':
@@ -127,26 +128,20 @@ DOCUMENTATION :: END
% for row in top_stat['rows']:
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${stat_id}" data-rating_key="${row.get('rating_key')}" data-title="${row.get('title')}"
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}"
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}"
data-user_id="${row.get('user_id')}" data-friendly_name="${row.get('friendly_name')}" data-user_thumb="${row.get('user_thumb')}"
data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}">
<div class="sub-list">${loop.index + 1}</div>
<div class="sub-value">
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
% if top_stat['rows'][loop.index]['rating_key']:
<a href="info?rating_key=${row['rating_key']}" title="${row['title']}">
<% href = 'info?rating_key={}'.format(row['rating_key']) if row['rating_key'] else '#' %>
<a href="${href}" title="${row['title']}">
${row['title']}
</a>
% else:
${row['title']}
% endif
% elif stat_id == 'top_users':
% if top_stat['rows'][loop.index]['user_id']:
<a href="user?user_id=${row['user_id']}" title="${row['friendly_name']}">
<% user_href = 'user?user_id={}'.format(row['user_id']) if row['user_id'] else '#' %>
<a href="${user_href}" title="${row['friendly_name']}">
${row['friendly_name']}
</a>
% else:
${row['friendly_name']}
% endif
% elif stat_id == 'top_platforms':
${row['platform']}
% elif stat_id == 'most_concurrent':
@@ -182,32 +177,47 @@ DOCUMENTATION :: END
var stat_id = $(elem).data('stat_id');
var art = $(elem).data('art');
var thumb = $(elem).data('thumb');
var user_id = $(elem).data('user_id');
var user_thumb = $(elem).data('user_thumb');
var rating_key = $(elem).data('rating_key');
var [height, fallback] = ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) ? [300, 'cover'] : [450, 'poster'];
var href;
if (stat_id == 'most_concurrent') {
return
} else if (stat_id == 'top_users') {
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (thumb || 'images/gravatar-default.png') + ')');
$('#stats-thumb-url-' + stat_id).attr('href', 'user?user_id=' + $(elem).data('user_id')).prop('title', $(elem).data('friendly_name'));
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
if (user_id) {
href = 'user?user_id=' + user_id;
} else {
href = '#';
}
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('friendly_name'));
} else if (stat_id == 'top_platforms') {
$('#stats-thumb-' + stat_id).removeClass(function (index, className) {
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
}).addClass('platform-' + $(elem).data('platform'));
$('#stats-background-' + stat_id).removeClass(function (index, className) {
return (className.match (/(^|\s)platform-\S+/g) || []).join(' ');
}).addClass('platform-' + $(elem).data('platform'));
}).addClass('platform-' + $(elem).data('platform') + '-rgba');
} else {
$('#stats-thumb-url-' + stat_id).attr('href', 'info?rating_key=' + $(elem).data('rating_key')).prop('title', $(elem).data('title'));
if (rating_key) {
href = 'info?rating_key=' + rating_key;
} else {
href = '#';
}
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
if (art) {
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&fallback=art)');
$('#stats-background-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art)');
} else {
$('#stats-background-' + stat_id).css('background-image', 'url(images/art.png)');
}
if (thumb) {
$('#stats-thumb-' + stat_id).css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&fallback=' + fallback + ')');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(pms_image_proxy?img=' + thumb + '&width=300&height=' + height + '&opacity=60&background=282828&blur=3&fallback=' + fallback + ')');
} else {
$('#stats-background-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
$('#stats-thumb-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(images/' + fallback + '.png)');
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.5"/>
<TileColor>#282a2d</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 971 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -2,17 +2,17 @@
"name": "Tautulli",
"icons": [
{
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0",
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.5",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.0",
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.5",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#1f1f1f",
"background_color": "#1f1f1f",
"theme_color": "#282a2d",
"background_color": "#282a2d",
"display": "standalone"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

View File

@@ -1,70 +1 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="275.000000pt" height="275.000000pt" viewBox="0 0 275.000000 275.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,275.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M2200 2569 c0 -4 -12 -11 -27 -17 -57 -20 -143 -130 -149 -187 -7
-82 -6 -105 6 -140 7 -22 23 -56 36 -75 13 -19 20 -37 16 -40 -5 -3 -11 -13
-15 -23 -4 -11 -12 -15 -19 -11 -7 4 -10 4 -6 -1 10 -11 -20 -65 -37 -65 -8 0
-12 -6 -9 -14 3 -7 -2 -16 -11 -20 -9 -3 -14 -10 -11 -15 4 -5 -5 -18 -19 -30
-15 -11 -26 -23 -27 -28 0 -4 -4 -7 -9 -6 -4 2 -6 -3 -4 -10 3 -6 -6 -24 -20
-39 -14 -16 -23 -28 -20 -28 4 0 0 -7 -7 -15 -30 -33 -38 -45 -38 -55 0 -5 -4
-10 -8 -10 -5 0 -17 -16 -28 -35 -10 -19 -22 -35 -26 -35 -5 0 -8 -5 -8 -10 0
-10 -8 -22 -37 -55 -7 -7 -13 -17 -13 -22 0 -5 -13 -23 -30 -41 -16 -18 -31
-36 -32 -40 -2 -4 -6 -6 -10 -5 -4 2 -4 -5 -1 -14 4 -10 2 -14 -4 -10 -6 4
-13 -3 -17 -16 -3 -12 -16 -34 -30 -48 -14 -15 -22 -29 -19 -33 3 -3 -2 -8
-11 -12 -9 -3 -16 -12 -16 -19 0 -6 -11 -24 -25 -39 -13 -14 -24 -30 -24 -33
0 -4 -5 -9 -12 -12 -6 -2 -9 1 -6 6 3 5 -2 9 -12 8 -10 0 -45 2 -79 5 -34 3
-62 2 -62 -2 0 -4 -7 -8 -15 -8 -21 0 -95 -33 -95 -43 0 -4 -5 -5 -12 -1 -7 5
-8 3 -4 -5 5 -8 0 -14 -13 -18 -11 -3 -18 -9 -15 -14 3 -5 1 -9 -4 -9 -8 0
-43 -60 -48 -82 -1 -5 -3 -10 -4 -13 -1 -3 -4 -9 -5 -15 -1 -5 -4 -13 -5 -17
-1 -5 -1 -17 0 -28 1 -16 -5 -13 -28 15 -17 19 -28 38 -25 43 2 4 1 6 -4 5
-14 -5 -114 112 -107 124 4 6 3 8 -3 5 -5 -3 -44 33 -85 81 -42 48 -90 103
-107 122 -55 61 -72 88 -61 95 34 21 50 139 30 210 -6 22 -12 48 -13 57 -1 9
-5 14 -9 12 -4 -3 -8 -2 -8 3 0 17 -70 84 -110 104 -26 14 -67 24 -110 27 -57
4 -76 1 -121 -20 -30 -14 -67 -38 -83 -54 -33 -34 -70 -84 -61 -84 3 0 -1 -10
-9 -22 -23 -37 -18 -147 8 -205 61 -130 212 -195 343 -149 38 13 41 13 59 -7
10 -12 17 -23 15 -25 -1 -2 8 -13 21 -24 13 -11 22 -25 20 -32 -2 -6 -1 -8 3
-4 7 7 35 -15 35 -27 0 -4 14 -20 30 -35 17 -15 30 -32 30 -38 0 -5 7 -13 16
-16 8 -3 12 -11 8 -17 -4 -7 -3 -9 3 -6 11 7 49 -42 45 -56 -1 -5 1 -6 6 -3 8
5 49 -35 43 -43 -1 -2 4 -8 13 -13 9 -5 13 -14 10 -20 -4 -6 -3 -8 3 -5 11 7
45 -28 36 -37 -4 -3 2 -6 12 -6 10 0 15 -3 12 -6 -4 -4 4 -16 17 -28 13 -12
31 -33 40 -47 15 -24 15 -27 0 -33 -8 -3 -21 -3 -26 1 -7 3 -8 1 -4 -5 4 -7
-2 -16 -16 -22 -13 -6 -43 -24 -68 -40 -25 -16 -56 -35 -70 -42 -14 -7 -28
-17 -31 -22 -4 -5 -12 -5 -19 -1 -9 5 -11 4 -6 -3 4 -7 3 -12 -1 -12 -5 0 -33
-16 -63 -35 -61 -40 -63 -40 -82 -22 -7 8 -24 19 -36 26 -12 7 -20 17 -16 23
4 7 3 8 -4 4 -6 -4 -31 -2 -54 5 -49 14 -128 18 -128 7 0 -5 -7 -8 -16 -8 -28
0 -80 -30 -120 -69 -43 -43 -65 -68 -64 -76 1 -27 -1 -35 -9 -31 -5 3 -8 -32
-7 -82 2 -128 46 -201 155 -256 96 -49 228 -27 309 52 17 16 34 27 37 24 4 -4
5 -2 4 3 -4 15 11 56 20 51 6 -4 11 18 14 62 0 6 4 12 8 12 4 0 5 8 3 18 -2 9
-5 36 -7 59 -4 40 -2 44 32 65 20 12 39 21 44 20 4 -1 7 3 7 9 0 6 7 8 17 5
10 -4 14 -4 11 0 -6 5 54 54 67 54 2 0 24 14 49 31 25 17 51 27 57 23 8 -4 9
-3 5 5 -4 6 0 14 11 18 10 3 30 14 44 24 14 10 29 16 33 12 3 -3 6 -1 6 5 0 7
7 12 15 12 8 0 15 -7 15 -15 0 -8 -5 -15 -12 -15 -6 0 -9 -2 -6 -5 3 -3 0 -24
-6 -47 -7 -23 -10 -59 -6 -83 4 -23 6 -52 5 -63 -1 -12 2 -19 6 -17 3 3 13
-10 20 -28 19 -47 95 -113 152 -131 123 -40 255 4 319 106 22 36 45 44 60 23
5 -6 8 -5 8 2 0 9 7 10 24 3 13 -5 44 -11 67 -15 24 -3 76 -12 114 -19 39 -8
86 -17 106 -20 33 -6 38 -11 44 -43 12 -67 50 -124 113 -176 12 -9 26 -17 30
-17 4 0 19 -6 34 -14 15 -8 56 -14 95 -14 245 0 365 295 190 468 -31 31 -64
53 -92 61 -24 7 -42 15 -40 18 2 4 -20 5 -48 4 -29 -1 -52 -2 -52 -3 0 -1 -10
-3 -23 -6 -61 -11 -182 -105 -182 -141 0 -9 -8 -12 -22 -8 -26 5 -184 33 -238
41 -19 3 -42 8 -50 11 -15 6 -35 10 -70 12 -14 1 -20 8 -20 26 0 29 -39 115
-60 132 -12 10 -13 15 -2 30 24 34 61 117 56 125 -3 4 0 8 6 8 6 0 9 4 6 8 -3
5 -6 34 -7 65 -1 31 -6 57 -11 57 -4 0 -8 7 -8 16 0 9 -9 29 -21 45 -17 24
-18 33 -9 44 7 8 16 12 21 9 5 -3 6 2 3 10 -3 9 -1 16 5 16 6 0 10 6 8 13 -1
6 4 11 11 9 8 -1 11 1 8 6 -7 12 39 74 48 65 4 -4 4 0 0 10 -3 10 -1 17 5 17
6 0 11 7 11 15 0 8 7 15 15 15 8 0 14 3 13 8 -3 10 110 180 199 297 5 6 26 36
47 67 21 32 43 55 47 52 5 -3 6 1 4 8 -3 8 3 23 13 34 11 11 30 36 43 56 13
19 40 58 59 86 l35 52 45 -7 c25 -3 50 -6 55 -6 78 5 153 38 199 87 68 72 96
198 63 279 -9 20 -14 37 -11 37 5 0 -43 55 -76 86 -14 13 -28 24 -33 24 -4 0
-5 5 -1 12 4 7 3 8 -4 4 -6 -4 -24 0 -40 8 -28 15 -192 19 -192 5z"/>
<path d="M2046 2132 c-3 -5 1 -9 9 -9 8 0 12 4 9 9 -3 4 -7 8 -9 8 -2 0 -6 -4
-9 -8z"/>
</g>
</svg>
<svg version="1" xmlns="http://www.w3.org/2000/svg" width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000" preserveAspectRatio="xMidYMid meet"><g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)" fill="#000000" stroke="none"><path d="M5695 6555 c-135 -34 -244 -94 -342 -189 -40 -39 -73 -76 -73 -83 0 -7 -4 -13 -10 -13 -14 0 -87 -156 -106 -225 -22 -83 -26 -234 -8 -320 17 -79 86 -230 133 -288 l30 -39 -48 -71 c-39 -57 -159 -228 -251 -357 -69 -97 -398 -564 -416 -590 -13 -19 -60 -87 -105 -150 -45 -63 -107 -151 -138 -195 -30 -44 -59 -84 -63 -90 -7 -9 -251 -354 -346 -490 -92 -131 -173 -245 -175 -245 -1 0 -34 9 -72 21 -130 38 -325 31 -454 -18 -168 -63 -313 -196 -385 -354 -39 -87 -65 -183 -68 -256 0 -24 -3 -43 -4 -43 -2 0 -43 46 -91 102 -49 57 -100 117 -115 133 -14 17 -128 149 -253 295 -125 146 -251 292 -279 324 -56 65 -77 89 -108 126 -58 68 -152 178 -172 200 -12 14 -50 57 -83 96 l-61 71 27 44 c58 93 91 217 92 342 2 161 -38 294 -125 412 -133 181 -316 279 -542 292 -470 27 -833 -434 -699 -887 74 -251 275 -437 530 -490 132 -28 334 -6 421 45 l42 24 173 -197 c96 -108 186 -210 200 -227 15 -16 163 -187 330 -380 458 -529 491 -567 526 -605 18 -19 31 -35 30 -36 -6 -5 -265 -161 -277 -167 -8 -4 -34 -20 -58 -35 -194 -124 -634 -382 -651 -382 -12 0 -46 20 -75 44 -60 49 -180 112 -242 127 -21 5 -48 12 -59 15 -11 4 -65 9 -121 11 -81 4 -117 1 -182 -15 -261 -66 -462 -270 -528 -537 -10 -40 -11 -217 -2 -258 5 -23 11 -51 14 -61 29 -145 147 -312 284 -403 123 -82 224 -114 370 -118 83 -3 124 2 240 29 36 9 133 57 187 94 60 41 111 91 153 152 14 19 28 37 32 40 19 15 71 140 89 217 17 73 20 107 16 198 -4 61 -7 121 -9 134 -3 28 -46 0 482 321 179 108 379 228 444 265 104 59 120 65 133 52 13 -13 12 -22 -10 -78 -49 -123 -58 -165 -62 -262 -7 -149 25 -286 89 -383 47 -72 91 -128 125 -158 19 -17 39 -36 45 -42 27 -25 136 -94 150 -94 8 0 17 -4 20 -9 3 -5 16 -11 28 -14 13 -3 50 -12 83 -21 74 -19 278 -15 345 7 198 65 358 196 435 358 16 34 20 36 49 28 17 -4 49 -10 71 -14 22 -3 99 -16 170 -30 72 -13 144 -26 160 -29 28 -5 101 -18 170 -31 17 -3 80 -14 140 -25 61 -11 124 -22 140 -25 17 -4 49 -9 72 -12 40 -5 42 -7 48 -47 14 -98 29 -147 73 -235 36 -75 61 -110 121 -171 154 -154 280 -210 480 -213 134 -2 180 5 273 40 212 83 371 262 427 481 24 93 25 255 2 342 -64 241 -245 428 -481 501 -62 18 -97 23 -200 22 -107 0 -136 -4 -205 -26 -44 -15 -109 -43 -145 -64 -83 -48 -208 -171 -250 -245 -17 -32 -35 -60 -38 -61 -4 -2 -46 4 -93 13 -48 10 -104 20 -125 23 -22 3 -46 8 -54 11 -8 3 -33 7 -55 10 -38 5 -58 9 -122 21 -16 3 -53 10 -83 15 -30 6 -66 12 -79 15 -13 2 -103 19 -200 36 -169 30 -207 42 -196 60 10 16 -28 155 -62 224 -19 39 -54 96 -78 127 l-45 58 40 52 c96 125 143 266 143 433 1 164 -27 263 -108 391 -19 30 -35 57 -35 61 0 3 31 49 69 102 57 81 450 638 625 889 28 40 62 88 76 107 14 18 194 274 400 568 291 414 379 534 393 531 10 -2 27 -6 37 -9 78 -25 240 -29 338 -9 433 87 677 573 489 974 -93 200 -255 332 -478 389 -87 22 -227 25 -304 6z"/></g></svg>

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="900px"
height="300px" viewBox="0 0 900 300" enable-background="new 0 0 900 300" xml:space="preserve">
<g id="Layer_1">
<g>
<path fill="none" d="M401.343,203.396c7.493,0,14.836,0,22.407,0c-3.766-12.354-7.495-24.586-11.333-37.176
C408.665,178.818,405.017,191.064,401.343,203.396z"/>
<path fill="#FFFFFF" d="M436.772,189.627c-4.399-12.521-8.814-25.034-13.224-37.553c-7.552,0-15.105,0-22.657,0.002
c-0.14,0.611-0.221,1.246-0.428,1.836c-8.502,24.335-17.017,48.666-25.526,72.996c-1.45,4.146-2.887,8.298-4.362,12.535
c6.686,0,13.198,0,19.792,0c2.095-6.873,4.172-13.693,6.248-20.514c10.593,0,21.056,0,31.594,0
c2.102,6.894,4.177,13.707,6.245,20.489c6.66,0,13.146,0,19.772,0c-0.229-0.676-0.408-1.213-0.597-1.747
C448.014,221.656,442.4,205.639,436.772,189.627z M401.343,203.396c3.674-12.332,7.322-24.578,11.074-37.176
c3.837,12.59,7.567,24.82,11.333,37.176C416.179,203.396,408.836,203.396,401.343,203.396z"/>
<path fill="#FFFFFF" d="M371.072,152.459c-21.916,0-43.668,0-65.421,0c0,5.111,0,10.227,0,15.341c7.836,0,15.672,0,23.691,0
c0,24.046,0,47.786,0,71.621c6.164,0,12.144,0,18.361,0c0-23.901,0-47.698,0-71.682c7.948,0,15.66,0,23.369,0
C371.072,162.549,371.072,157.549,371.072,152.459z"/>
<path fill="#FFFFFF" d="M532.615,152.555c-6.176,0-12.154,0-18.349,0c0,0.842,0,1.592,0,2.34c0,16.42,0.021,32.838-0.021,49.258
c-0.006,2.675-0.119,5.373-0.494,8.018c-0.997,7.07-4.634,11.153-11.06,12.587c-3.731,0.83-7.491,0.754-11.238,0
c-5.745-1.157-9.344-4.668-10.58-10.297c-0.678-3.088-0.958-6.317-0.976-9.487c-0.094-16.738-0.045-33.478-0.045-50.217
c0-0.732,0-1.467,0-2.148c-6.296,0-12.275,0-18.329,0c0,0.591,0,1.035,0,1.48c0,18.209-0.013,36.416,0.018,54.625
c0.003,1.908,0.127,3.835,0.396,5.727c1.632,11.408,7.683,19.563,18.562,23.516c11.146,4.052,22.489,3.83,33.593-0.393
c7.937-3.017,13.483-8.627,16.483-16.617c1.474-3.93,2.044-8.024,2.042-12.211c-0.006-18.146-0.002-36.289-0.002-54.436
C532.615,153.744,532.615,153.188,532.615,152.555z"/>
<path fill="#FFFFFF" d="M688.532,152.555c-6.16,0-12.14,0-18.349,0c0,0.924,0,1.62,0,2.318c0,16.482,0.021,32.967-0.021,49.449
c-0.006,2.609-0.125,5.244-0.49,7.824c-1.003,7.127-4.74,11.291-11.226,12.654c-3.674,0.771-7.373,0.702-11.056-0.037
c-5.74-1.156-9.354-4.652-10.596-10.281c-0.682-3.086-0.964-6.317-0.979-9.487c-0.094-16.738-0.044-33.478-0.044-50.216
c0-0.739,0-1.478,0-2.18c-6.278,0-12.26,0-18.332,0c0,0.582,0,1.024,0,1.467c0.003,18.271-0.011,36.543,0.021,54.815
c0.002,1.847,0.132,3.707,0.391,5.533c1.626,11.411,7.669,19.567,18.546,23.533c11.213,4.09,22.621,3.836,33.771-0.445
c7.851-3.019,13.345-8.612,16.315-16.541c1.521-4.052,2.06-8.274,2.056-12.592c-0.018-17.953-0.008-35.905-0.008-53.858
C688.532,153.893,688.532,153.27,688.532,152.555z"/>
<path fill="#FFFFFF" d="M542.405,167.793c7.813,0,15.522,0,23.479,0c0,23.979,0,47.771,0,71.648c6.172,0,12.102,0,18.425,0
c0-23.948,0-47.738,0-71.729c7.947,0,15.655,0,23.378,0c0-5.158,0-10.115,0-15.115c-21.819,0-43.53,0-65.281,0
C542.405,157.691,542.405,162.648,542.405,167.793z"/>
<path fill="#FFFFFF" d="M725.407,152.58c-6.199,0-12.177,0-18.193,0c0,29.021,0,57.92,0,86.857c17.832,0,35.528,0,53.335,0
c0-5.191,0-10.146,0-15.307c-11.72,0-23.322,0-35.142,0C725.407,200.143,725.407,176.361,725.407,152.58z"/>
<path fill="#FFFFFF" d="M790.668,152.593c-6.197,0-12.172,0-18.216,0c0,28.985,0,57.833,0,86.831c17.905,0,35.6,0,53.371,0
c0-5.105,0-10.107,0-15.316c-11.729,0-23.335,0-35.155,0C790.668,200.119,790.668,176.339,790.668,152.593z"/>
<path fill="#FFFFFF" d="M838.183,239.464c6.083,0,12.021,0,18.096,0c0-29.09,0-57.987,0-86.879c-6.12,0-12.098,0-18.096,0
C838.183,181.611,838.183,210.514,838.183,239.464z"/>
<ellipse fill="#E5A00D" cx="847.229" cy="129.416" rx="12.459" ry="12.44"/>
</g>
</g>
<g id="Layer_2_1_">
<g>
<image overflow="visible" opacity="0.2" width="279" height="216" xlink:href="13AD46AC.png" transform="matrix(1 0 0 1 15.5 73.5)">
</image>
<g>
<path fill="#FFFFFF" d="M243.77,212.601c-10.482,0-19.533,6.118-23.783,14.977l-37.755-5.478
c-0.133-14.445-11.881-26.114-26.356-26.114c-5.163,0-9.975,1.489-14.041,4.051l-53.649-60.787
c3.091-4.319,4.914-9.605,4.914-15.319c0-14.56-11.802-26.362-26.363-26.362c-14.56,0-26.363,11.803-26.363,26.362
c0,14.559,11.803,26.361,26.363,26.361c4.484,0,8.704-1.122,12.399-3.096l54.223,61.438c-2.439,3.994-3.846,8.688-3.846,13.712
c0,14.56,11.803,26.363,26.364,26.363c10.422,0,19.43-6.05,23.708-14.828l37.832,5.491c0.22,14.369,11.929,25.953,26.354,25.953
c14.56,0,26.363-11.804,26.363-26.362S258.33,212.601,243.77,212.601z"/>
</g>
</g>
<g>
<image overflow="visible" opacity="0.2" width="279" height="279" xlink:href="13AD46AD.png" transform="matrix(1 0 0 1 15.5 10.5)">
</image>
<g>
<path fill="#E5A00D" d="M243.77,34.779c-14.56,0-26.363,11.802-26.363,26.362c0,7.17,2.867,13.667,7.512,18.42l-58.424,82.548
c-3.25-1.431-6.84-2.23-10.619-2.23c-14.56,0-26.363,11.8-26.363,26.36c0,2.592,0.38,5.094,1.078,7.46L86.2,221.181
c-4.82-5.271-11.754-8.58-19.461-8.58c-14.56,0-26.363,11.803-26.363,26.361s11.803,26.362,26.363,26.362
c14.561,0,26.363-11.804,26.363-26.362c0-2.518-0.36-4.951-1.019-7.256l44.477-27.534c4.815,5.183,11.686,8.429,19.318,8.429
c14.561,0,26.364-11.803,26.364-26.361c0-6.424-2.301-12.31-6.119-16.883l58.973-83.322c2.72,0.948,5.637,1.468,8.677,1.468
c14.561,0,26.363-11.802,26.363-26.361C270.135,46.582,258.33,34.779,243.77,34.779z"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="298.961px" height="300px" viewBox="0 0 298.961 300" enable-background="new 0 0 298.961 300" xml:space="preserve">
<g>
<image overflow="visible" opacity="0.2" width="289" height="224" xlink:href="93124B54.png" transform="matrix(1 0 0 1 4.9805 70.5)">
</image>
<g>
<path fill="#FFFFFF" d="M241.811,215.25c-10.935,0-20.375,6.382-24.809,15.623l-39.383-5.715
c-0.139-15.068-12.393-27.242-27.493-27.242c-5.385,0-10.405,1.554-14.646,4.229l-55.963-63.414
c3.224-4.505,5.127-10.02,5.127-15.981c0-15.188-12.312-27.5-27.5-27.5s-27.5,12.312-27.5,27.5s12.312,27.5,27.5,27.5
c4.677,0,9.079-1.171,12.934-3.23l56.56,64.089c-2.544,4.168-4.012,9.064-4.012,14.307c0,15.188,12.312,27.5,27.5,27.5
c10.872,0,20.269-6.311,24.731-15.467l39.463,5.727c0.229,14.99,12.443,27.074,27.49,27.074c15.188,0,27.5-12.313,27.5-27.5
S256.998,215.25,241.811,215.25z"/>
</g>
</g>
<g>
<image overflow="visible" opacity="0.2" width="289" height="289" xlink:href="93124B55.png" transform="matrix(1 0 0 1 4.9805 5.5)">
</image>
<g>
<path fill="#E5A00D" d="M241.811,29.75c-15.188,0-27.5,12.312-27.5,27.5c0,7.48,2.99,14.258,7.836,19.216l-60.943,86.113
c-3.389-1.493-7.135-2.329-11.076-2.329c-15.188,0-27.5,12.313-27.5,27.5c0,2.704,0.397,5.314,1.125,7.783l-46.306,28.668
c-5.028-5.5-12.261-8.951-20.3-8.951c-15.188,0-27.5,12.313-27.5,27.5s12.312,27.5,27.5,27.5s27.5-12.313,27.5-27.5
c0-2.627-0.376-5.165-1.064-7.57l46.396-28.724c5.022,5.407,12.189,8.794,20.15,8.794c15.188,0,27.5-12.313,27.5-27.5
c0-6.701-2.399-12.84-6.382-17.611l61.515-86.92c2.837,0.988,5.88,1.532,9.052,1.532c15.188,0,27.5-12.312,27.5-27.5
S256.998,29.75,241.811,29.75z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="956.251px" height="214.137px" viewBox="0 -12.083 956.251 214.137" enable-background="new 0 -12.083 956.251 214.137"
xml:space="preserve">
<g>
<path fill="none" d="M165.163,137.282c12.933,0,25.607,0,38.673,0c-6.5-21.354-12.936-42.492-19.56-64.253
C177.798,94.804,171.503,115.967,165.163,137.282z"/>
<path fill="#FFFFFF" d="M226.314,113.482c-7.594-21.64-15.215-43.269-22.824-64.903c-13.035,0-26.072,0-39.106,0.002
c-0.241,1.061-0.381,2.153-0.737,3.174c-14.675,42.061-29.37,84.111-44.058,126.164c-2.503,7.166-4.984,14.34-7.53,21.666
c11.54,0,22.78,0,34.162,0c3.614-11.881,7.2-23.67,10.784-35.455c18.283,0,36.34,0,54.529,0
c3.627,11.914,7.212,23.691,10.781,35.412c11.493,0,22.693,0,34.128,0c-0.397-1.166-0.705-2.094-1.029-3.02
C245.716,168.84,236.026,141.157,226.314,113.482z M165.163,137.282c6.34-21.314,12.635-42.478,19.113-64.253
c6.624,21.761,13.06,42.899,19.56,64.253C190.77,137.282,178.096,137.282,165.163,137.282z"/>
<path fill="#FFFFFF" d="M112.916,49.243c-37.826,0-75.371,0-112.916,0c0,8.836,0,17.674,0.001,26.514c13.525,0,27.05,0,40.889,0
c0,41.563,0,82.594,0,123.787c10.64,0,20.961,0,31.692,0c0-41.311,0-82.438,0-123.892c13.718,0,27.028,0,40.334,0
C112.916,66.68,112.916,58.04,112.916,49.243z"/>
<path fill="#FFFFFF" d="M391.734,49.409c-10.657,0-20.976,0-31.667,0c0,1.457,0,2.752,0,4.045c0,28.379,0.037,56.758-0.037,85.135
c-0.012,4.623-0.206,9.287-0.851,13.857c-1.723,12.219-7.998,19.277-19.089,21.752c-6.441,1.436-12.932,1.305-19.396,0.002
c-9.917-2-16.129-8.066-18.262-17.797c-1.171-5.336-1.655-10.92-1.685-16.398c-0.158-28.928-0.074-57.859-0.074-86.791
c0-1.268,0-2.534,0-3.717c-10.869,0-21.188,0-31.639,0c0,1.023,0,1.793,0,2.563c0,31.471-0.018,62.939,0.032,94.41
c0.005,3.301,0.217,6.629,0.683,9.896c2.816,19.719,13.26,33.813,32.036,40.645c19.239,7.002,38.813,6.619,57.978-0.678
c13.699-5.215,23.273-14.91,28.45-28.723c2.545-6.791,3.528-13.869,3.526-21.104c-0.011-31.363-0.005-62.723-0.005-94.082
C391.734,51.463,391.734,50.502,391.734,49.409z"/>
<path fill="#FFFFFF" d="M660.845,52.793c0-1.073,0-2.149,0-3.385c-10.631,0-20.953,0-31.668,0c0,1.598,0,2.803,0,4.008
c0,28.487,0.035,56.979-0.037,85.467c-0.012,4.51-0.215,9.064-0.844,13.525c-1.734,12.313-8.184,19.514-19.379,21.869
c-6.34,1.332-12.723,1.215-19.076-0.064c-9.912-1.996-16.146-8.039-18.291-17.77c-1.174-5.334-1.662-10.92-1.689-16.398
c-0.16-28.928-0.076-57.86-0.076-86.791c0-1.277,0-2.553,0-3.77c-10.834,0-21.158,0-31.641,0c0,1.01,0,1.771,0,2.537
c0.004,31.581-0.016,63.16,0.033,94.742c0.006,3.189,0.23,6.406,0.678,9.564c2.805,19.723,13.236,33.82,32.006,40.674
c19.354,7.068,39.047,6.631,58.289-0.77c13.549-5.217,23.031-14.889,28.16-28.59c2.623-7.006,3.557-14.305,3.551-21.764
C660.828,114.852,660.845,83.823,660.845,52.793z"/>
<path fill="#FFFFFF" d="M408.633,75.745c13.484,0,26.794,0,40.523,0c0,41.445,0,82.566,0,123.836c10.655,0,20.887,0,31.802,0
c0-41.391,0-82.51,0-123.974c13.717,0,27.02,0,40.35,0c0-8.916,0-17.481,0-26.125c-37.66,0-75.133,0-112.675,0
C408.633,58.288,408.633,66.853,408.633,75.745z"/>
<path fill="#FFFFFF" d="M724.49,49.454c-10.699,0-21.014,0-31.4,0c0,50.158,0,100.104,0,150.119c30.779,0,61.322,0,92.055,0
c0-8.973,0-17.535,0-26.453c-20.227,0-40.254,0-60.652,0C724.49,131.659,724.49,90.556,724.49,49.454z"/>
<path fill="#FFFFFF" d="M837.128,49.476c-10.697,0-21.01,0-31.441,0c0,50.094,0,99.954,0,150.073c30.904,0,61.445,0,92.119,0
c0-8.826,0-17.469,0-26.473c-20.246,0-40.273,0-60.678,0C837.128,131.618,837.128,90.515,837.128,49.476z"/>
<path fill="#FFFFFF" d="M919.136,199.62c10.502,0,20.748,0,31.232,0c0-50.277,0-100.226,0-150.158c-10.564,0-20.879,0-31.232,0
C919.136,99.631,919.136,149.583,919.136,199.62z"/>
<circle fill="#E5A00D" cx="934.751" cy="9.417" r="21.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -11,10 +11,29 @@
<div class="row">
<div class="col-md-12">
<div class="padded-header" id="current-activity-header">
<h3>Activity &nbsp;&nbsp;<small><span id="currentActivityHeader"></span></small></h3>
<h3><span id="sessions-shortcut">Activity</span> &nbsp;&nbsp;
<small>
<span id="currentActivityHeader" style="display: none;">
Streams: <span id="currentActivityHeader-streams"></span> |
Bandwidth: <span id="currentActivityHeader-bandwidth"></span>
<span id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span>
</span>
</small>
</h3>
</div>
<div id="currentActivity">
<% from plexpy import PLEX_SERVER_UP %>
% if PLEX_SERVER_UP:
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div>
% elif config['pms_is_cloud']:
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
% else:
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin':
Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.
% endif
</div>
% endif
</div>
</div>
</div>
@@ -23,27 +42,29 @@
<div class="col-md-12">
<div class="home-padded-header padded-header">
<h3 class="pull-left">Watch Statistics</h3>
<div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px">
% if config['home_stats_type'] == 0:
<label class="btn btn-dark active">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off" checked> Play Count
</label>
<label class="btn btn-dark">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off"> Play Duration
</label>
% else:
<label class="btn btn-dark">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off"> Play Count
</label>
<label class="btn btn-dark active">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off" checked> Play Duration
</label>
% endif
</div>
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
<span class="input-group-addon btn-dark inactive">days</span>
<div class="button-bar">
<div class="btn-group pull-left" data-toggle="buttons" id="watch-stats-toggles" style="margin-right: 3px">
% if config['home_stats_type'] == 0:
<label class="btn btn-dark active">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off" checked> Play Count
</label>
<label class="btn btn-dark">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off"> Play Duration
</label>
% else:
<label class="btn btn-dark">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-plays" value="0" autocomplete="off"> Play Count
</label>
<label class="btn btn-dark active">
<input type="radio" class="watched-stats-toggle" name="watched-stats-type" id="watched-stats-duration" value="1" autocomplete="off" checked> Play Duration
</label>
% endif
</div>
<div class="input-group pull-left" style="width: 1px; margin-right: 3px" id="watched-stats-days-selection">
<span class="input-group-addon btn-dark inactive">Last</span>
<input type="number" class="form-control number-input" name="watched-stats-days" id="watched-stats-days" value="${config['home_stats_length']}" min="1" data-default="30" data-toggle="tooltip" title="Min: 1 day" />
<span class="input-group-addon btn-dark inactive">days</span>
</div>
</div>
</div>
</div>
@@ -61,7 +82,9 @@
<div class="col-md-12">
<div class="home-padded-header padded-header" id="library-statistics-header">
<h3 class="pull-left">Library Statistics</h3>
<span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span>
<div class="button-bar">
<span class="btn btn-dark active" style="cursor: default">${config['pms_name']}</span>
</div>
</div>
</div>
</div>
@@ -86,23 +109,25 @@
<a href="#" id="recently-added-page-right" class="paginate btn-gray disabled" data-id="-1"><i class="fa fa-lg fa-chevron-right"></i></a>
</li>
</ul>
<div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px">
<label class="btn btn-dark active" id="recently-added-label-all">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="" autocomplete="off"> All
</label>
<label class="btn btn-dark" id="recently-added-label-movies">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies
</label>
<label class="btn btn-dark" id="recently-added-label-tv">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows
</label>
<label class="btn btn-dark" id="recently-added-label-music">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-music" value="artist" autocomplete="off"> Music
</label>
</div>
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection">
<input type="number" class="form-control" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="100" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 100 items" />
<span class="input-group-addon btn-dark inactive">items</span>
<div class="button-bar">
<div class="btn-group pull-left" data-toggle="buttons" id="recently-added-toggles" style="margin-right: 3px">
<label class="btn btn-dark active" id="recently-added-label-all">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-all" value="" autocomplete="off"> All
</label>
<label class="btn btn-dark" id="recently-added-label-movies">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-movie" value="movie" autocomplete="off"> Movies
</label>
<label class="btn btn-dark" id="recently-added-label-tv">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-show" value="show" autocomplete="off"> TV Shows
</label>
<label class="btn btn-dark" id="recently-added-label-music">
<input type="radio" name="recently-added-toggle" id="recently-added-toggle-music" value="artist" autocomplete="off"> Music
</label>
</div>
<div class="input-group pull-left" style="width: 1px;" id="recently-added-count-selection">
<input type="number" class="form-control number-input" name="recently-added-count" id="recently-added-count" value="${config['home_stats_recently_added_count']}" min="1" max="50" data-default="50" data-toggle="tooltip" title="Min: 1 item<br>Max: 50 items" />
<span class="input-group-addon btn-dark inactive">items</span>
</div>
</div>
</div>
</div>
@@ -123,12 +148,13 @@
<%def name="modalIncludes()">
% if _session['user_group'] == 'admin' and config['update_show_changelog']:
<% from plexpy.common import RELEASE %>
<div id="changelog-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="changelog-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Tautulli Updated</h4>
<h4 class="modal-title">Tautulli Updated to <strong>${RELEASE}</strong></h4>
</div>
<div class="modal-body">
</div>
@@ -199,6 +225,7 @@
<script src="${http_root}js/moment-with-locale.js"></script>
<script src="${http_root}js/jquery.scrollbar.min.js"></script>
<script src="${http_root}js/jquery.mousewheel.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script>
<script>
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
@@ -225,9 +252,10 @@
});
}
});
};
}
</script>
% if 'current_activity' in config['home_sections']:
<% from plexpy import PLEX_SERVER_UP %>
% if 'current_activity' in config['home_sections'] and PLEX_SERVER_UP:
<script>
var defaultHandler = {
get: function(target, name) {
@@ -235,7 +263,14 @@
}
};
var create_instances = [];
var activity_ready = true;
$('#currentActivityHeader-bandwidth-tooltip').tooltip({ container: 'body', placement: 'right', delay: 50 });
function getCurrentActivity() {
activity_ready = false;
$.ajax({
url: 'get_activity',
type: 'GET',
@@ -243,6 +278,7 @@
async: true,
error: function (xhr, status, error) {
console.log(status + ': ' + error);
activity_ready = true;
},
complete: function (xhr, status) {
$('#dashboard-checking-activity').remove();
@@ -256,8 +292,13 @@
}
if (!(current_activity)) {
$('#currentActivityHeader').text('');
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.</div>');
% if _session['user_group'] == 'admin':
var msg_settings = ' Check the <a href="logs">logs</a> and verify your server connection in the <a href="settings#tab_tabs-plex_media_server">settings</a>.';
% else:
var msg_settings = '';
% endif
$('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.' + msg_settings + '</div>');
return
}
@@ -270,63 +311,82 @@
// Update the header stream counts
var sc_dp = current_activity.stream_count_direct_play,
sc_ds = current_activity.stream_count_direct_stream,
sc_tc = current_activity.stream_count_transcode;
var header_count = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
sc_tc = current_activity.stream_count_transcode,
total_bw = current_activity.total_bandwidth,
lan_bw = current_activity.lan_bandwidth,
wan_bw = current_activity.wan_bandwidth;
var streams_header = stream_count + ' stream' + (stream_count > 1 ? 's' : '') + ' (';
if (sc_dp) {
header_count += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
streams_header += sc_dp + ' direct play' + (sc_dp > 1 ? 's' : '') + ', ';
}
if (sc_ds) {
header_count += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', ';
streams_header += sc_ds + ' direct stream' + (sc_ds > 1 ? 's' : '') + ', ';
}
if (sc_tc) {
header_count += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', ';
streams_header += sc_tc + ' transcode' + (sc_tc > 1 ? 's' : '') + ', ';
}
header_count = header_count.replace(/, $/, '');
header_count += ')';
$('#currentActivityHeader').text(header_count);
streams_header = streams_header.replace(/, $/, '') + ')';
$('#currentActivityHeader-streams').text(streams_header);
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps'));
var lan_wan_bandwidth_header = '';
if (lan_bw) {
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', ';
}
if (wan_bw) {
lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', ';
}
if (lan_wan_bandwidth_header) {
bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')';
}
$('#currentActivityHeader-bandwidth').text(bandwidth_header);
$('#currentActivityHeader').show();
sessions.forEach(function (session) {
var s = new Proxy(session, defaultHandler);
var s = (typeof Proxy === "function") ? new Proxy(session, defaultHandler) : session;
var key = s.session_key;
var session_id = s.session_id;
var instance = $('#activity-instance-' + key);
// Create a new instance if it doesn't exist
if (!(instance.length)) {
getActivityInstance(session);
create_instances.push(key);
getActivityInstance(key);
return;
}
// Update play state icon
var state_icon = '';
switch (s.state) {
case 'playing':
var state_icon = '<i class="fa fa-fw fa-play"></i>&nbsp;';
state_icon = '<i class="fa fa-fw fa-play"></i>&nbsp;';
break;
case 'paused':
var state_icon = '<i class="fa fa-fw fa-pause"></i>&nbsp;';
state_icon = '<i class="fa fa-fw fa-pause"></i>&nbsp;';
break;
case 'buffering':
var state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;';
state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;';
break;
default:
var state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;';
state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;';
}
$('#play-state-' + key).html(state_icon).attr('title', capitalizeFirstLetter(s.state));
// Switching tracks can be under the same session key, so need to update the info.
if (s.media_type === 'track') {
// Update if artist changed
if (s.grandparent_rating_key != instance.data('grandparent_rating_key')) {
$('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&fallback=art&refresh=true)');
if (s.grandparent_rating_key !== instance.data('grandparent_rating_key')) {
$('#background-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.art + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true)');
$('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key)
.attr('title', s.grandparent_title)
.text(s.grandparent_title);
}
// Update cover if album changed
if (s.parent_rating_key != instance.data('parent_rating_key')) {
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
$('#poster-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
$('#poster-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)');
$('#poster-' + key + '-bg').css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&opacity=60&background=282828&blur=3&fallback=poster&refresh=true)');
$('#poster-url-' + key)
.attr('href', 'info?rating_key=' + s.parent_rating_key)
.attr('title', s.parent_title);
@@ -336,7 +396,7 @@
.text(s.parent_title);
}
// Update cover if track changed
if (s.parent_rating_key != instance.data('parent_rating_key')) {
if (s.parent_rating_key !== instance.data('parent_rating_key')) {
$('#metadata-title-' + key)
.attr('href', 'info?rating_key=' + s.rating_key)
.attr('title', s.title)
@@ -347,55 +407,51 @@
// Update the transcode state
var transcode_decision = '';
if (s.transcode_decision === 'transcode') {
var throttled = (s.transcode_throttled == 1) ? ' (Throttled)' : ' (Speed: ' + s.transcode_speed + ')';
var throttled = (s.transcode_throttled === 1) ? ' (Throttled)' : ' (Speed: ' + s.transcode_speed + ')';
transcode_decision = 'Transcode' + throttled;
} else if (s.transcode_decision === 'copy') {
transcode_decision = 'Direct Stream';
} else {
transcode_decision = 'Direct Play' + ((s.synced_version == 1) ? ' (Synced)' : '');
transcode_decision = 'Direct Play';
}
$('#transcode_decision-' + key).html(transcode_decision);
var transcode_container = '';
if (s.stream_container_decision === 'transcode') {
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' &rarr; ' + s.stream_container.toUpperCase() + ')';
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
} else {
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')';
}
$('#transcode_container-' + key).html(transcode_container);
var video_decision = '';
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.video_decision != '') {
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.video_decision !== '') {
var v_res= '';
switch (s.video_resolution.toLowerCase()) {
case 'sd':
var v_res = 'SD';
v_res = 'SD';
break;
case '4k':
var v_res = '4k';
v_res = '4k';
break;
default:
var v_res = s.video_resolution + 'p'
v_res = s.video_resolution + 'p'
}
var sv_res = '';
switch (s.stream_video_resolution.toLowerCase()) {
case 'sd':
var sv_res = 'SD';
sv_res = 'SD';
break;
case '4k':
var sv_res = '4k';
sv_res = '4k';
break;
default:
var sv_res = s.stream_video_resolution + 'p'
sv_res = s.stream_video_resolution + 'p'
}
if (s.stream_video_decision === 'transcode') {
var hw_d = '';
var hw_e = '';
if (s.transcode_hw_requested === 1 && s.transcode_hw_full_pipeline === 0) {
hw_d = ' (HW)';
} else if (s.transcode_hw_requested === 1 && s.transcode_hw_full_pipeline === 1) {
hw_d = ' (HW)';
hw_e = ' (HW)';
}
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' &rarr; ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
var hw_e = (s.transcode_hw_encoding === 1) ? ' (HW)' : '';
video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + ')';
} else if (s.stream_video_decision === 'copy') {
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')';
} else {
@@ -407,11 +463,11 @@
$('#video_decision-' + key).html(video_decision);
var audio_decision = '';
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.audio_codec) {
if (['movie', 'episode', 'clip', 'track'].indexOf(s.media_type) > -1 && s.audio_decision) {
var a_codec = (s.audio_codec === 'truehd') ? 'TrueHD' : s.audio_codec.toUpperCase();
var sa_codec = (s.stream_audio_codec === 'truehd') ? 'TrueHD' : s.stream_audio_codec.toUpperCase();
if (s.stream_audio_decision === 'transcode') {
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' &rarr; ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
audio_decision = 'Transcode (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ' <i class="fa fa-long-arrow-right"></i> ' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
} else if (s.stream_audio_decision === 'copy') {
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
} else {
@@ -423,19 +479,19 @@
var subtitle_decision = 'None';
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
if (s.stream_subtitle_decision === 'transcode') {
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' &rarr; ' + s.stream_subtitle_codec.toUpperCase() + ')';
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')';
} else if (s.stream_subtitle_decision === 'copy') {
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
} else if (s.stream_subtitle_decision === 'burn') {
subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')';
} else {
subtitle_decision = 'Direct Play (' + s.subtitle_codec.toUpperCase() + ')';
subtitle_decision = 'Direct Play (' + ((s.synced_version === '1') ? s.stream_subtitle_codec.toUpperCase() : s.subtitle_codec.toUpperCase()) + ')';
}
}
$('#subtitle_decision-' + key).html(subtitle_decision);
// Update the stream quality profile and bandwidth
if (s.media_type != 'photo' && s.quality_profile != 'Unknown') {
if (s.media_type !== 'photo' && s.quality_profile !== 'Unknown') {
var br = parseInt(s.stream_bitrate) || '';
if (br) {
if (br > 1000) {
@@ -448,33 +504,36 @@
} else {
$('#stream_quality-' + key).html(s.quality_profile);
}
$('#optimized_version-' + key).html(s.optimized_version_profile);
$('#optimized_version-' + key).html(s.optimized_version_profile + ' (' + s.optimized_version_title + ')');
$('#synced_quality_profile-' + key).html(s.synced_quality_profile);
if (s.media_type != 'photo' && parseInt(s.bandwidth)) {
var bw = parseInt(s.bandwidth);
if (bw != "Unknown") {
if (bw > 1000) {
bw = (bw / 1000).toFixed(1) + ' Mbps';
} else {
bw = bw + ' kbps'
}
$('#location-' + key).html(s.location.toUpperCase());
if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') {
var bw = parseInt(s.bandwidth) || 0;
if (bw > 1000) {
bw = (bw / 1000).toFixed(1) + ' Mbps';
} else {
bw = bw + ' kbps'
}
$('#stream-bandwidth-' + key).html(bw);
}
};
// Update the stream progress times
$('#stream-eta-' + key).html(moment().add(parseInt(s.duration) - parseInt(s.view_offset), 'milliseconds').format(time_format));
$('#stream-view-offset-' + key).data('state', s.state);
if ($('#stream-view-offset-' + key).data('last_view_offset') != s.view_offset) {
$('#stream-view-offset-' + key).data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
var stream_view_offset = $('#stream-view-offset-' + key);
stream_view_offset.data('state', s.state);
if (stream_view_offset.data('last_view_offset') !== s.view_offset) {
stream_view_offset.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
}
// Update the progress bars, percent - 3 because of 3px padding-right
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
$('#progress-bar-' + key).data('state', s.state);
if ($('#progress-bar-' + key).data('last_view_offset') != s.view_offset) {
$('#progress-bar-' + key).data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
var progress_bar = $('#progress-bar-' + key);
progress_bar.data('state', s.state);
if (progress_bar.data('last_view_offset') && progress_bar.data('last_view_offset') !== s.view_offset) {
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset);
}
// Add temporary class so we know which instances are still active
@@ -492,34 +551,45 @@
});
} else {
$('#currentActivityHeader').text('');
$('#currentActivityHeader').hide();
$('#currentActivity').html('<div id="dashboard-no-activity" class="text-muted">Nothing is currently being played.</div>');
}
activity_ready = true;
}
});
}
function getActivityInstance(session) {
function getActivityInstance(session_key) {
$.ajax({
url: 'get_current_activity_instance',
type: 'GET',
cache: false,
async: true,
data: session,
data: {
session_key: session_key
},
complete: function(xhr, status) {
$('#currentActivity').append(xhr.responseText);
$('#activity-instance-' + session.session_key + ' .dashboard-activity-info-scroller').scrollbar();
$('#activity-instance-' + session.session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 })
$('#terminate-button-' + session.session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
lockScroll('#activity-instance-' + session.session_key + ' .dashboard-activity-info-scroller');
$('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar();
$('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 });
$('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller');
var index = create_instances.indexOf(session_key);
if (index > -1) {
create_instances.splice(index, 1);
}
}
});
}
getCurrentActivity();
setInterval(function () {
getCurrentActivity();
}, 2000);
if (!(create_instances.length) && activity_ready) {
getCurrentActivity();
}
}, ${config['home_refresh_interval'] * 1000});
setInterval(function(){
$('.progress_time_offset').each(function () {
@@ -534,7 +604,7 @@
if ($(this).data('state') === 'playing' && $(this).data('view_offset') >= 0) {
var view_offset = parseInt($(this).data('view_offset'));
var stream_duration = parseInt($(this).data('stream_duration'));
var progress_percent = Math.min(Math.trunc(view_offset / stream_duration * 100), 100)
var progress_percent = Math.min(Math.trunc(view_offset / stream_duration * 100), 100);
$(this).width(progress_percent - 3 + '%').html(progress_percent + '%')
.attr('data-original-title', 'Stream Progress ' + progress_percent + '%')
.data('view_offset', Math.min(view_offset + 1000, stream_duration));
@@ -609,6 +679,12 @@
});
});
});
$('#sessions-shortcut').on('tripleclick', function () {
$.getJSON('return_sessions_url', function(sessions_url) {
window.open(sessions_url, '_blank');
});
});
% endif
</script>
% endif
@@ -726,13 +802,13 @@
leftTotal = Math.max(Math.min(leftTotal + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotal }, 250);
if (leftTotal == 0) {
if (leftTotal === 0) {
$("#recently-added-page-left").addClass("disabled").blur();
} else {
$("#recently-added-page-left").removeClass("disabled");
}
if (leftTotal == leftMax) {
if (leftTotal === leftMax) {
$("#recently-added-page-right").addClass("disabled").blur();
} else {
$("#recently-added-page-right").removeClass("disabled");
@@ -764,7 +840,7 @@
$.ajax({
url: 'get_changelog',
data: {
latest_only: true,
since_prev_release: true,
update_shown: true
},
cache: false,

View File

@@ -38,20 +38,21 @@ DOCUMENTATION :: END
<%!
import re
from plexpy import common, notifiers
from plexpy import notifiers
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
# Get audio codec file
def af(codec):
for pattern, file in common.MEDIA_FLAGS_AUDIO.iteritems():
for pattern, file_type in MEDIA_FLAGS_AUDIO.iteritems():
if re.match(pattern, codec):
return file
return file_type
return codec
# Get audio codec file
def vf(codec):
for pattern, file in common.MEDIA_FLAGS_VIDEO.iteritems():
for pattern, file_type in MEDIA_FLAGS_VIDEO.iteritems():
if re.match(pattern, codec):
return file
return file_type
return codec
def br(text):
@@ -63,7 +64,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">
@@ -80,7 +81,7 @@ DOCUMENTATION :: END
<div class="col-md-12">
<div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb">
% if data['media_type'] == 'movie':
% if data['media_type'] in ('movie', 'collection'):
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li>
<li class="active">${data['title']}</li>
% elif data['media_type'] == 'show':
@@ -116,9 +117,9 @@ DOCUMENTATION :: END
<div class="col-md-9">
<div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track':
<a href="${config['pms_web_url'] or 'https://app.plex.tv/desktop'}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View in Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['parent_rating_key']}" target="_blank" title="View on Plex Web">
% else:
<a href="${config['pms_web_url'] or 'https://app.plex.tv/desktop'}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View in Plex Web">
<a href="${config['pms_web_url']}#!/server/${config['pms_identifier']}/details?key=%2Flibrary%2Fmetadata%2F${data['rating_key']}" target="_blank" title="View on Plex Web">
% endif
% 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=art);">
@@ -151,7 +152,7 @@ DOCUMENTATION :: END
</a>
</div>
<div class="summary-content-title">
% if data['media_type'] in ('movie', 'show', 'artist'):
% if data['media_type'] in ('movie', 'show', 'artist', 'collection'):
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['media_type'] == 'season':
<h1>&nbsp;</h1><h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1>
@@ -175,7 +176,7 @@ DOCUMENTATION :: END
<div class="col-md-9">
% if data['media_type'] == 'movie':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
% elif data['media_type'] == 'show' or data['media_type'] == 'season':
% elif data['media_type'] in ('show', 'season', 'collection'):
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
% elif data['media_type'] == 'episode':
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 70px;">
@@ -235,6 +236,8 @@ DOCUMENTATION :: END
Aired <strong> <span id="airdate">${data['originally_available_at']}</span></strong>
% elif data['media_type'] == 'album' or data['media_type'] == 'track':
Released <strong> ${data['year']}</strong>
% elif data['media_type'] == 'collection':
Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
% endif
</div>
<div class="summary-content-details-tag">
@@ -308,51 +311,65 @@ DOCUMENTATION :: END
</div>
% if data['media_type'] == 'show':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Season List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading season list...</div>
</div>
</div>
% elif data['media_type'] == 'season':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Episode List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading episode list...</div>
</div>
</div>
% elif data['media_type'] == 'artist':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Album List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading album list...</div>
</div>
</div>
% elif data['media_type'] == 'album':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>Track List for <strong>${data['title']}</strong></span>
</div>
</div>
<div class='table-card-back'>
<div id="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading track list...</div>
</div>
</div>
% endif
% elif data['media_type'] == 'collection':
<div class="col-md-12">
<div class='table-card-header'>
<div class="table-card-header">
<div class="header-bar">
<span>${MEDIA_TYPE_HEADERS[data['sub_media_type']]} in <strong>${data['title']}</strong> collection</span>
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list"><i class="fa fa-refresh fa-spin"></i>&nbsp; Loading movies list...</div>
</div>
</div>
<div id="collection-related-list-container" style="display: none;">
</div>
% endif
% if data['media_type'] != 'collection':
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span>Watch History for <strong>${data['title']}</strong></span>
</div>
@@ -371,16 +388,26 @@ DOCUMENTATION :: END
</a>
</div>
% endif
% if data.get('tvmaze_id') or data.get('themoviedb_id'):
<div class="btn-group">
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-lookup-info"
data-id="${data['grandparent_rating_key'] if data['media_type'] in ('episode', 'track') else data['parent_rating_key'] if data['media_type'] in ('season', 'album') else data['rating_key']}"
data-title="${data['grandparent_title'] if data['media_type'] in ('episode', 'track') else data['parent_title'] if data['media_type'] in ('season', 'album') else data['title']}">
<i class="fa fa-search"></i> Delete Lookup Info
</button>
</div>
% endif
% if data.get('poster_url'):
<div class="btn-group">
% if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<span class="imgur-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="80" data-width="80" style="display: inline-flex;">
<span class="hosted-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="80" data-width="80" style="display: inline-flex;">
% else:
<span class="imgur-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="120" data-width="80" style="display: inline-flex;">
<span class="hosted-poster-tooltip" data-toggle="popover" data-img="${data['poster_url']}" data-height="120" data-width="80" style="display: inline-flex;">
% endif
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-imgur-poster"
data-id="${data['parent_rating_key'] if data['media_type'] in ('episode', 'track') else data['rating_key']}">
<i class="fa fa-picture-o"></i> Reset Imgur Poster
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-hosted-poster"
data-id="${data['parent_rating_key'] if data['media_type'] in ('episode', 'track') else data['rating_key']}"
data-title="${data["poster_title"]}">
<i class="fa fa-picture-o"></i> Delete ${data['img_service']} Poster
</button>
</span>
</div>
@@ -420,6 +447,7 @@ DOCUMENTATION :: END
</table>
</div>
</div>
% endif
</div>
</div>
</div>
@@ -428,7 +456,7 @@ DOCUMENTATION :: END
</%def>
<%def name="modalIncludes()">
<div id="info-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="info-modal">
<div class="modal fade" id="info-modal" tabindex="-1" role="dialog" aria-labelledby="info-modal">
</div>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
</div>
@@ -493,6 +521,7 @@ DOCUMENTATION :: END
% endfor
</select>
</div>
<p class="help-block">Note: All custom notification conditions will be bypassed.</p>
</div>
</div>
<div class="modal-footer">
@@ -518,12 +547,12 @@ DOCUMENTATION :: END
function get_history() {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
grandparent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}
@@ -534,12 +563,12 @@ DOCUMENTATION :: END
function get_history() {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
parent_rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}
@@ -550,18 +579,19 @@ DOCUMENTATION :: END
function get_history() {
history_table_options.ajax = {
url: 'get_history',
type: 'post',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
rating_key: "${data['rating_key']}",
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}
}
</script>
% endif
% if data['media_type'] != 'collection':
<script>
$(document).ready(function () {
get_history();
@@ -638,7 +668,8 @@ DOCUMENTATION :: END
});
});
</script>
% if data['media_type'] in ('show', 'season', 'artist', 'album'):
% endif
% if data['media_type'] in ('show', 'season', 'artist', 'album', 'collection'):
<script>
$.ajax({
url: 'get_item_children',
@@ -646,7 +677,24 @@ DOCUMENTATION :: END
async: true,
data: { rating_key : "${data['rating_key']}" },
complete: function(xhr, status) {
$("#children-list").html(xhr.responseText); }
$("#children-list").html(xhr.responseText);
}
});
</script>
% endif
% if data['media_type'] == 'collection':
<script>
$.ajax({
url: 'get_item_children_related',
type: 'GET',
async: true,
data: {
rating_key : "${data['rating_key']}",
title: "${data['title']}"
},
complete: function(xhr, status) {
$("#collection-related-list-container").html(xhr.responseText).show();
}
});
</script>
% endif
@@ -657,7 +705,7 @@ DOCUMENTATION :: END
</script>
% if data.get('poster_url'):
<script>
$('.imgur-poster-tooltip').popover({
$('.hosted-poster-tooltip').popover({
html: true,
container: 'body',
trigger: 'hover',
@@ -668,14 +716,29 @@ DOCUMENTATION :: END
}
});
$('#delete-imgur-poster').on('click', function () {
var msg = 'Are you sure you want to reset the Imgur poster for <strong>${data["poster_title"]}</strong>?';
var url = 'delete_poster_url';
var data = { rating_key: $(this).data('id') }
$('#delete-hosted-poster').on('click', function () {
var msg = 'Are you sure you want to delete the ${data['img_service']} poster for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
'All previous links to this image will no longer work.';
var url = 'delete_hosted_images';
var data = { rating_key: $(this).data('id') };
var callback = function () {
$('.imgur-poster-tooltip').popover('destroy');
$('#delete-imgur-poster').closest('span').remove();
}
$('.hosted-poster-tooltip').popover('destroy');
$('#delete-hosted-poster').closest('.btn-group').remove();
};
confirmAjaxCall(url, msg, data, false, callback);
});
</script>
% endif
% if data.get('tvmaze_id') or data.get('themoviedb_id'):
<script>
$('#delete-lookup-info').on('click', function () {
var msg = 'Are you sure you want to delete the 3rd party API lookup for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
'The info will be looked up again the next time a notification is sent.';
var url = 'delete_lookup_info';
var data = { rating_key: $(this).data('id'), title: $(this).data('title') };
var callback = function () {
$('#delete-lookup-info').closest('.btn-group').remove();
};
confirmAjaxCall(url, msg, data, false, callback);
});
</script>

View File

@@ -37,13 +37,42 @@ DOCUMENTATION :: END
% else:
<li>
% endif
%if data['children_type'] == 'season':
% if data['children_type'] == 'movie':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
<div class="item-children-instance-text-wrapper poster-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
% elif data['children_type'] == 'show':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
<div class="item-children-instance-text-wrapper poster-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
</div>
% elif data['children_type'] == 'season':
<a href="info?rating_key=${child['rating_key']}" title="Season ${child['media_index']}">
<div class="item-children-poster">
% if child['thumb']:
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);">
% else:
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=450&fallback=poster);">
% endif
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
@@ -59,7 +88,7 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'episode':
<a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}">
<div class="item-children-poster">
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);">
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
Episode ${child['media_index']}
@@ -79,13 +108,13 @@ DOCUMENTATION :: END
% elif data['children_type'] == 'album':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
<div class="item-children-instance-text-wrapper album-item">
<div class="item-children-instance-text-wrapper cover-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>

View File

@@ -0,0 +1,92 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: info_collection_list.html
Version: 0.1
Variable names: data [list]
data :: Usable parameters
== Global keys ==
children_type Returns the type of children in the array.
children_count Returns the number of episodes in the array.
children_list Returns an array of episodes.
data['children_list'] :: Usable paramaters
== Global keys ==
rating_key Returns the unique identifier for the media item.
media_index Returns the episode number.
title Returns the name of the episode.
thumb Returns the location of the item's thumbnail. Use with pms_image_proxy.
parent_thumb Returns the location of the item's parent thumbnail. Use with pms_image_proxy.
DOCUMENTATION :: END
</%doc>
% if data != None:
<%
from plexpy.common import MEDIA_TYPE_HEADERS
types = ('movie', 'show', 'artist', 'album')
%>
% for media_type in types:
% if data['results_list'][media_type]:
<div class="col-md-12">
<div class="table-card-header">
<div class="header-bar">
<span>${MEDIA_TYPE_HEADERS[media_type]} in <strong>${title}</strong> collection</span>
</div>
</div>
<div class="table-card-back">
<div id="children-list" class="children-list">
<div class="item-children-wrapper">
<ul class="item-children-instance list-unstyled">
% for child in data['results_list'][media_type]:
<li>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">
<div class="item-children-poster">
% if media_type in ('artist', 'album'):
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% else:
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% endif
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
</a>
% if media_type == 'artist':
<div class="item-children-instance-text-wrapper cover-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
</div>
% elif media_type == 'album':
<div class="item-children-instance-text-wrapper cover-item">
<h3>
<a href="info?rating_key=${child['parent_rating_key']}" title="${child['parent_title']}">${child['parent_title']}</a>
</h3>
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
</div>
% else:
<div class="item-children-instance-text-wrapper poster-item">
<h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a>
</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
% endif
</li>
% endfor
</ul>
</div>
</div>
</div>
</div>
% endif
% endfor
% endif

View File

@@ -54,6 +54,31 @@ DOCUMENTATION :: END
% if data != None:
% if data['results_count'] > 0:
% if 'collection' in data['results_list'] and data['results_list']['collection']:
<div class="item-children-wrapper">
<div class="item-children-section-title">
<h4>Collections</h4>
</div>
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['collection']:
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['min_year']} - ${child['max_year']}</h3>
</div>
</a>
</li>
% endfor
</ul>
</div>
% endif
% if 'movie' in data['results_list'] and data['results_list']['movie']:
<div class="item-children-wrapper">
<div class="item-children-section-title">
@@ -64,12 +89,12 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper season-item">
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
@@ -89,12 +114,12 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper season-item">
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['title']}">${child['title']}</h3>
<h3 class="text-muted">${child['year']}</h3>
</div>
@@ -114,12 +139,12 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face season-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
<div class="item-children-poster-face poster-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=450&fallback=poster);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
</div>
<div class="item-children-instance-text-wrapper season-item">
<div class="item-children-instance-text-wrapper poster-item">
<h3 title="${child['parent_title']}">${child['parent_title']}</h3>
<h3 class="text-muted">S${child['media_index']}</h3>
</div>
@@ -139,7 +164,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face episode-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
<div class="item-children-poster-face episode-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=500&height=250&fallback=art);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -165,7 +190,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="item-children-poster-face cover-item style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -189,7 +214,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['thumb']}&width=300&height=300&fallback=cover);"></div>
% if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif
@@ -214,7 +239,7 @@ DOCUMENTATION :: END
<li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}">
<div class="item-children-poster">
<div class="item-children-poster-face album-poster" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
<div class="item-children-poster-face cover-item" style="background-image: url(pms_image_proxy?img=${child['parent_thumb']}&width=300&height=300&fallback=cover);">
<div class="item-children-card-overlay">
<div class="item-children-overlay-text">
Track ${child['media_index']}

View File

@@ -2,7 +2,7 @@
PNotify.prototype.options.addclass = "stack-bottomright";
PNotify.prototype.options.buttons.closer_hover = false;
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo.png' }
PNotify.prototype.options.desktop = { desktop: true, icon: 'images/logo-circle.png' };
PNotify.prototype.options.history = false;
PNotify.prototype.options.shadow = false;
PNotify.prototype.options.stack = { dir1: 'up', dir2: 'left', firstpos1: 25, firstpos2: 25 };
@@ -21,7 +21,7 @@ function check_notifications() {
$.getJSON('get_browser_notifications', function (data) {
if (data) {
$.each(data, function (i, notification) {
if (notification.delay == 0) {
if (notification.delay === 0) {
PNotify.prototype.options.hide = false;
} else {
PNotify.prototype.options.hide = true;
@@ -34,7 +34,7 @@ function check_notifications() {
setTimeout(function () {
"use strict";
check_notifications();
}, 3000);
}, 5000);
}
$(document).ready(function () {

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
!function(t){function i(t){var i=jQuery(this);settings=jQuery.extend({},e,t.data);var a=i.data("triclick_clicks")||0,c=i.data("triclick_start")||0;0===a&&(c=t.timeStamp),0!=c&&t.timeStamp>c+settings.threshold&&(a=0,c=t.timeStamp),a+=1,3===a&&(a=0,c=0,t.type="tripleclick",void 0===jQuery.event.handle?jQuery.event.dispatch.apply(this,arguments):jQuery.event.handle.apply(this,arguments)),i.data("triclick_clicks",a),i.data("triclick_start",c)}var e={threshold:1e3};t.event.special.tripleclick={setup:function(e,a){t(this).bind("touchstart click.triple",e,i)},teardown:function(e){t(this).unbind("touchstart click.triple",i)}}}(jQuery);

View File

@@ -26,7 +26,7 @@ function refreshTab() {
function showMsg(msg, loader, timeout, ms, error) {
var feedback = $("#ajaxMsg");
update = $("#updatebar");
var update = $("#updatebar");
if (update.is(":visible")) {
var height = update.height() + 35;
feedback.css("bottom", height + "px");
@@ -35,7 +35,7 @@ function showMsg(msg, loader, timeout, ms, error) {
}
var message = $("<div class='msg'>" + msg + "</div>");
if (loader) {
var message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
message = $("<i class='fa fa-refresh fa-spin'></i> " + msg + "</div>");
feedback.css("padding", "14px 10px")
}
if (error) {
@@ -290,19 +290,13 @@ String.prototype.toProperCase = function () {
function millisecondsToMinutes(ms, roundToMinute) {
if (ms > 0) {
seconds = ms / 1000;
minutes = seconds / 60;
if (roundToMinute) {
output = Math.round(minutes, 0)
} else {
minutesFloor = Math.floor(minutes);
secondsReal = Math.round((seconds - (minutesFloor * 60)), 0);
if (secondsReal < 10) {
secondsReal = '0' + secondsReal;
}
output = minutesFloor + ':' + secondsReal;
}
return output;
var minutes = Math.floor(ms / 60000);
var seconds = ((ms % 60000) / 1000).toFixed(0);
if (roundToMinute) {
return (seconds >= 30 ? (minutes + 1) : minutes);
} else {
return (seconds == 60 ? (minutes + 1) + ":00" : minutes + ":" + (seconds < 10 ? "0" : "") + seconds);
}
} else {
if (roundToMinute) {
return '0';

View File

@@ -21,7 +21,7 @@ history_table_options = {
"infoFiltered": "<span class='hidden-md hidden-sm hidden-xs'>(filtered from _MAX_ total entries)</span>",
"emptyTable": "No data in table",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
},
},
"pagingType": "full_numbers",
"stateSave": true,
"processing": false,
@@ -59,6 +59,8 @@ history_table_options = {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Paused"><i class="fa fa-pause fa-fw"></i></span>';
} else if (rowData['state'] === 'buffering') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-spinner fa-fw"></i></span>';
} else if (rowData['state'] === 'stopped') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
}
$(td).html('<div><div style="float: left;">' + state + '&nbsp;' + date + '</div></div>');
} else if (rowData['group_count'] > 1) {
@@ -170,7 +172,7 @@ history_table_options = {
},
"width": "33%",
"className": "datatable-wrap"
},
},
{
"targets": [7],
"data":"started",
@@ -203,7 +205,7 @@ history_table_options = {
"targets": [9],
"data":"stopped",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null || rowData['state'] != null) {
if (cellData === null || (rowData['state'] != null && rowData['state'] != "stopped")) {
$(td).html('n/a');
} else {
$(td).html(moment(cellData,"X").format(time_format));
@@ -268,7 +270,7 @@ history_table_options = {
});
if ($('#row-edit-mode').hasClass('active')) {
$('.delete-control').each(function () {
$('.history_table .delete-control').each(function () {
$(this).removeClass('hidden');
});
}
@@ -288,7 +290,9 @@ history_table_options = {
},
"preDrawCallback": function(settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0)
showMsg(msg, false, false, 0);
$('[data-toggle="tooltip"]').tooltip('destroy');
$('[data-toggle="popover"]').popover('destroy');
},
"rowCallback": function (row, rowData, rowIndex) {
if (rowData['group_count'] == 1) {
@@ -320,7 +324,7 @@ history_table_options = {
$(row).addClass('current-activity-row');
}
}
}
};
// Parent table platform modal
$('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
@@ -462,7 +466,7 @@ function childTableOptions(rowData) {
});
if ($('#row-edit-mode').hasClass('active')) {
$('.delete-control').each(function () {
$('.history_table .delete-control').each(function () {
$(this).removeClass('hidden');
});
}

View File

@@ -28,9 +28,7 @@ libraries_list_table_options = {
$(td).html('<div class="edit-library-toggles">' +
'<button class="btn btn-xs btn-warning delete-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button>&nbsp' +
'<button class="btn btn-xs btn-warning purge-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&nbsp' +
'<input type="checkbox" id="do_notify-' + rowData['section_id'] + '" name="do_notify" value="1" ' + rowData['do_notify'] + '><label class="edit-tooltip" for="do_notify-' + rowData['section_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['section_id'] + '" name="keep_history" value="1" ' + rowData['keep_history'] + '><label class="edit-tooltip" for="keep_history-' + rowData['section_id'] + '" data-toggle="tooltip" title="Toggle History"><i class="fa fa-history fa-lg fa-fw"></i></label>&nbsp' +
'<input type="checkbox" id="do_notify_created-' + rowData['section_id'] + '" name="do_notify_created" value="1" ' + rowData['do_notify_created'] + '><label class="edit-tooltip" for="do_notify_created-' + rowData['section_id'] + '" data-toggle="tooltip" title="Toggle Recently Added"><i class="fa fa-download fa-lg fa-fw"></i></label>&nbsp' +
'</div>');
},
"width": "7%",
@@ -258,15 +256,7 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
var row = libraries_list_table.row(tr);
var rowData = row.data();
var do_notify = 0;
var do_notify_created = 0;
var keep_history = 0;
if ($('#do_notify-' + rowData['section_id']).is(':checked')) {
do_notify = 1;
}
if ($('#do_notify_created-' + rowData['section_id']).is(':checked')) {
do_notify_created = 1;
}
if ($('#keep_history-' + rowData['section_id']).is(':checked')) {
keep_history = 1;
}
@@ -280,8 +270,6 @@ $('#libraries_list_table').on('change', 'td.edit-control > .edit-library-toggles
url: 'edit_library',
data: {
section_id: rowData['section_id'],
do_notify: do_notify,
do_notify_created: do_notify_created,
keep_history: keep_history,
custom_thumb: custom_thumb
},

View File

@@ -113,7 +113,7 @@ login_log_table_options = {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0)
}
}
};
$('.login_log_table').on('click', '> tbody > tr > td.modal-control-ip', function () {
var tr = $(this).closest('tr');

View File

@@ -54,7 +54,7 @@ media_info_table_options = {
} else if (rowData['media_type'] === 'album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'photo' && rowData['parent_rating_key'] == '') {
} else if (rowData['media_type'] === 'photo_album') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Photos"><i class="fa fa-plus-circle fa-fw"></i></span>';
$(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else {
@@ -68,7 +68,7 @@ media_info_table_options = {
},
{
"targets": [1],
"data": "title",
"data": "sort_title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
var parent_info = '';
@@ -77,32 +77,44 @@ media_info_table_options = {
if (rowData['media_type'] === 'movie') {
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'show') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="TV Show"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'season') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Season"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + cellData + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + cellData + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">E' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'artist') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Artist"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Album"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + cellData + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + cellData + '</span>'
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=300&fallback=cover" data-height="80" data-width="80">T' + rowData['media_index'] + ' - ' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><a href="info?rating_key=' + rowData['rating_key'] + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'photo_album') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo Album"><i class="fa fa-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else if (rowData['media_type'] === 'photo') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Photo"><i class="fa fa-picture-o fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=poster" data-height="120" data-width="80">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else if (rowData['media_type'] === 'clip') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Video"><i class="fa fa-video-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="pms_image_proxy?img=' + rowData['thumb'] + '&width=300&height=450&fallback=art" data-height="80" data-width="140">' + rowData['title'] + '</span>';
$(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else {
$(td).html(cellData);
}
@@ -335,7 +347,7 @@ function childTableOptionsMedia(rowData) {
case 'album':
section_type = 'track';
break;
case 'photo':
case 'photo_album':
section_type = 'picture';
break;
}

View File

@@ -0,0 +1,146 @@
newsletter_log_table_options = {
"destroy": true,
"serverSide": true,
"processing": false,
"pagingType": "full_numbers",
"order": [ 0, 'desc'],
"pageLength": 50,
"stateSave": true,
"language": {
"search":"Search: ",
"lengthMenu": "Show _MENU_ lines per page",
"emptyTable": "No log information available",
"info" :"Showing _START_ to _END_ of _TOTAL_ lines",
"infoEmpty": "Showing 0 to 0 of 0 lines",
"infoFiltered": "(filtered from _MAX_ total lines)",
"loadingRecords": '<i class="fa fa-refresh fa-spin"></i> Loading items...</div>'
},
"autoWidth": false,
"scrollX": true,
"columnDefs": [
{
"targets": [0],
"data": "timestamp",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(moment(cellData, "X").format('YYYY-MM-DD HH:mm:ss'));
}
},
"width": "10%",
"className": "no-wrap"
},
{
"targets": [1],
"data": "newsletter_id",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "5%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "agent_name",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "5%",
"className": "no-wrap"
},
{
"targets": [3],
"data": "notify_action",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "5%",
"className": "no-wrap"
},
{
"targets": [4],
"data": "subject_text",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "23%"
},
{
"targets": [5],
"data": "body_text",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "35%"
},
{
"targets": [6],
"data": "start_date",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "5%"
},
{
"targets": [7],
"data": "end_date",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "5%"
},
{
"targets": [8],
"data": "uuid",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html('<a href="newsletter/' + rowData['uuid'] + '" target="_blank">' + cellData + '</a>');
}
},
"width": "5%"
},
{
"targets": [9],
"data": "success",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === 1) {
$(td).html('<span class="success-tooltip" data-toggle="tooltip" title="Newsletter Sent"><i class="fa fa-lg fa-fw fa-check"></i></span>');
} else {
$(td).html('<span class="success-tooltip" data-toggle="tooltip" title="Newsletter Failed"><i class="fa fa-lg fa-fw fa-times"></i></span>');
}
},
"searchable": false,
"orderable": false,
"className": "no-wrap",
"width": "2%"
},
],
"drawCallback": function (settings) {
// Jump to top of page
//$('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
// Create the tooltips.
$('body').tooltip({
selector: '[data-toggle="tooltip"]',
container: 'body'
});
},
"preDrawCallback": function(settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0)
}
};

View File

@@ -86,7 +86,7 @@ notification_log_table_options = {
"targets": [6],
"data": "success",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData == 1) {
if (cellData === 1) {
$(td).html('<span class="success-tooltip" data-toggle="tooltip" title="Notification Sent"><i class="fa fa-lg fa-fw fa-check"></i></span>');
} else {
$(td).html('<span class="success-tooltip" data-toggle="tooltip" title="Notification Failed"><i class="fa fa-lg fa-fw fa-times"></i></span>');
@@ -113,4 +113,4 @@ notification_log_table_options = {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0)
}
}
};

View File

@@ -37,7 +37,6 @@ sync_table_options = {
"data": "state",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData === 'pending') {
$(td).addClass('currentlyWatching');
$(td).html('Pending...');
} else {
$(td).html(cellData.toProperCase());
@@ -47,7 +46,7 @@ sync_table_options = {
},
{
"targets": [2],
"data": "friendly_name",
"data": "user",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id']) {
@@ -63,10 +62,10 @@ sync_table_options = {
},
{
"targets": [3],
"data": "title",
"data": "sync_title",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['metadata_type'] !== '') {
if (rowData['rating_key']) {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
} else {
$(td).html(cellData);
@@ -74,7 +73,7 @@ sync_table_options = {
}
},
"className": "datatable-wrap"
},
},
{
"targets": [4],
"data": "metadata_type",
@@ -98,7 +97,7 @@ sync_table_options = {
"data": "total_size",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData > 0 ) {
megabytes = Math.round((cellData/1024)/1024, 0)
megabytes = Math.round((cellData/1024)/1024, 0);
$(td).html(megabytes + 'MB');
} else {
$(td).html('0MB');
@@ -139,19 +138,33 @@ sync_table_options = {
// $('html,body').scrollTop(0);
$('#ajaxMsg').fadeOut();
if ($('#sync-row-edit-mode').hasClass('active')) {
$('.sync_table .delete-control').each(function () {
$(this).removeClass('hidden');
});
}
},
"preDrawCallback": function (settings) {
var msg = "<i class='fa fa-refresh fa-spin'></i>&nbspFetching rows...";
showMsg(msg, false, false, 0)
},
"rowCallback": function (row, rowData, rowIndex) {
if (rowData['state'] === 'pending') {
$(row).addClass('current-activity-row');
}
}
}
};
$('#sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
$('.sync_table').on('click', 'td.delete-control > .edit-sync-toggles > button.delete-sync', function () {
var tr = $(this).parents('tr');
var row = sync_table.row(tr);
var rowData = row.data();
var index_delete = syncs_to_delete.findIndex(x => x.client_id == rowData['client_id'] && x.sync_id == rowData['sync_id']);
var index_delete = syncs_to_delete.findIndex(function (x) {
return x.client_id === rowData['client_id'] && x.sync_id === rowData['sync_id'];
});
if (index_delete === -1) {
syncs_to_delete.push({ client_id: rowData['client_id'], sync_id: rowData['sync_id'] });

View File

@@ -45,7 +45,6 @@ users_list_table_options = {
$(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' +
'<input type="checkbox" id="allow_guest-' + rowData['user_id'] + '" name="allow_guest" value="1" ' + rowData['allow_guest'] + '><label class="edit-tooltip" for="allow_guest-' + rowData['user_id'] + '" data-toggle="tooltip" title="Toggle Guest Access"><i class="fa fa-unlock-alt fa-lg fa-fw"></i></label>&nbsp' +
'</div>');
@@ -284,12 +283,8 @@ $('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > inpu
var row = users_list_table.row(tr);
var rowData = row.data();
var do_notify = 0;
var keep_history = 0;
var allow_guest = 0;
if ($('#do_notify-' + rowData['user_id']).is(':checked')) {
do_notify = 1;
}
if ($('#keep_history-' + rowData['user_id']).is(':checked')) {
keep_history = 1;
}
@@ -304,7 +299,6 @@ $('#users_list_table').on('change', 'td.edit-control > .edit-user-toggles > inpu
data: {
user_id: rowData['user_id'],
friendly_name: friendly_name,
do_notify: do_notify,
keep_history: keep_history,
allow_guest: allow_guest,
thumb: rowData['user_thumb']

View File

@@ -3,22 +3,11 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">
<div class='container-fluid'>
% if config['update_section_ids'] == 1:
<div id="update_section_ids_message" style="text-align: center; margin-top: 20px;">
<i class="fa fa-exclamation-triangle"></i> Tautulli needs to update the Library IDs in your databse. Click the "<strong>Refresh libraries</strong>" button below to begin the update.
</div>
% elif config['update_section_ids'] == -1:
<div id="update_section_ids_message" style="text-align: center; margin-top: 20px;">
<i class="fa fa-refresh fa-spin"></i> Tautulli is updating library IDs in the database. This could take a few minutes to hours depending on the size of your database.
<br />
You may leave this page and come back later.
</div>
% endif
<div class='table-card-header'>
<div class="header-bar">
<span><i class="fa fa-book"></i> All Libraries</span>
@@ -31,16 +20,10 @@
<i class="fa fa-pencil"></i> Edit mode
</button>&nbsp
</div>
% if config['update_section_ids'] == -1:
<div class="btn-group">
<button class="btn btn-dark refresh-libraries-button" id="refresh-libraries-list" disabled><i class="fa fa-refresh"></i> Refresh libraries</button>
</div>
% else:
<div class="btn-group">
<button class="btn btn-dark refresh-libraries-button" id="refresh-libraries-list"><i class="fa fa-refresh"></i> Refresh libraries</button>
</div>
% endif
% endif
<div class="btn-group colvis-button-bar"></div>
</div>
</div>
@@ -108,7 +91,7 @@
json_data: JSON.stringify(d)
};
}
}
};
libraries_list_table = $('#libraries_list_table').DataTable(libraries_list_table_options);
var colvis = new $.fn.dataTable.ColVis(libraries_list_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 1] });
@@ -197,26 +180,20 @@
% if _session['user_group'] == 'admin':
$("#refresh-libraries-list").click(function () {
if ("${config['update_section_ids']}" == "1") {
$('#update_section_ids_message').html(
'<i class="fa fa-refresh fa-spin"></i> Tautulli is updating library IDs in the database. This could take a few minutes to hours depending on the size of your database.' +
'<br />' +
'You may leave this page and come back later.');
$(this).prop('disabled', true);
}
showMsg('Refreshing libraries list...', true, false);
$.ajax({
url: 'refresh_libraries_list',
cache: false,
async: true,
success: function (data) {
showMsg('<i class="fa fa-refresh"></i>&nbspLibraries list refresh started...', false, true, 2000, false);
},
complete: function (data) {
showMsg('<i class="fa fa-check"></i>&nbspLibraries list refreshed.', false, true, 2000, false);
},
error: function (jqXHR, textStatus, errorThrown) {
showMsg('<i class="fa fa-exclamation-circle"></i>&nbspUnable to refresh libraries list.', false, true, 2000, true);
complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = result.message;
if (result.result == 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
libraries_list_table.draw();
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 2000, true);
}
}
});
});

View File

@@ -30,7 +30,7 @@ DOCUMENTATION :: END
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/dataTables.colVis.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
</%def>
<%def name="body()">
@@ -71,11 +71,11 @@ DOCUMENTATION :: END
% endif
</div>
<div class="user-info-nav">
<ul class="user-info-nav">
<li class="active"><a href="#profile" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#libraryHistory" data-toggle="tab">History</a></li>
<ul class="user-info-nav" role="tablist">
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li>
<li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
% if _session['user_group'] == 'admin':
<li><a id="media-info-tab-btn" href="#libraryMediaInfo" data-toggle="tab">Media Info</a></li>
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" data-toggle="tab">Media Info</a></li>
% endif
</ul>
</div>
@@ -83,7 +83,7 @@ DOCUMENTATION :: END
</div>
</div>
<div class="tab-content">
<div class="tab-pane active" id="profile">
<div role="tabpanel" class="tab-pane active" id="tabs-profile">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -169,7 +169,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="libraryHistory">
<div role="tabpanel" class="tab-pane" id="tabs-history">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -221,7 +221,7 @@ DOCUMENTATION :: END
</div>
</div>
</div>
<div class="tab-pane" id="libraryMediaInfo">
<div role="tabpanel" class="tab-pane" id="tabs-mediainfo">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
@@ -366,8 +366,257 @@ DOCUMENTATION :: END
<script src="${http_root}js/tables/history_table.js${cache_param}"></script>
<script src="${http_root}js/tables/media_info_table.js${cache_param}"></script>
<script>
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
function loadHistoryTable() {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
};
history_table = $('#history_table-SID-${data["section_id"]}').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('#button-bar-history');
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
}
$('a[href="#tabs-history"]').on('shown.bs.tab', function() {
if (typeof(history_table) === 'undefined') {
loadHistoryTable();
}
});
$("#refresh-history-list").click(function () {
history_table.draw();
});
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
media_info_table_options.ajax = {
url: 'get_library_media_info',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
refresh: refresh_table
};
}
};
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-media-info');
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
}
$('a[href="#tabs-mediainfo"]').on('shown.bs.tab', function() {
if (typeof(media_info_table) === 'undefined') {
loadMediaInfoTable();
}
});
$("#refresh-media-info-table").click(function () {
media_info_child_table = {};
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
refresh_table = false;
});
$("#edit-library-tooltip").tooltip();
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
% endif
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'library_recently_watched',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-watched").html(xhr.responseText);
highlightWatchedScrollerButton();
}
});
}
function recentlyAdded() {
// Populate recently added
$.ajax({
url: 'library_recently_added',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-added").html(xhr.responseText);
highlightAddedScrollerButton();
}
});
}
recentlyWatched();
recentlyAdded();
function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-watched").width()) {
$("#recently-watched-page-right").removeClass("disabled");
} else {
$("#recently-watched-page-right").addClass("disabled");
}
}
function highlightAddedScrollerButton() {
var scroller = $("#recently-added-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-added").width()) {
$("#recently-added-page-right").removeClass("disabled");
} else {
$("#recently-added-page-right").addClass("disabled");
}
}
$(window).resize(function() {
highlightWatchedScrollerButton();
highlightAddedScrollerButton();
});
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
var leftTotalWatched = 0;
$(".paginate-watched").click(function (e) {
e.preventDefault();
var scroller = $("#recently-watched-row-scroller");
var containerWidth = $("#library-recently-watched").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalWatched }, 250);
if (leftTotalWatched == 0) {
$("#recently-watched-page-left").addClass("disabled").blur();
} else {
$("#recently-watched-page-left").removeClass("disabled");
}
if (leftTotalWatched == leftMax) {
$("#recently-watched-page-right").addClass("disabled").blur();
} else {
$("#recently-watched-page-right").removeClass("disabled");
}
});
var leftTotalAdded = 0;
$(".paginate-added").click(function (e) {
e.preventDefault();
var scroller = $("#recently-added-row-scroller");
var containerWidth = $("#library-recently-added").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalAdded }, 250);
if (leftTotalAdded == 0) {
$("#recently-added-page-left").addClass("disabled").blur();
} else {
$("#recently-added-page-left").removeClass("disabled");
}
if (leftTotalAdded == leftMax) {
$("#recently-added-page-right").addClass("disabled").blur();
} else {
$("#recently-added-page-right").removeClass("disabled");
}
});
$(document).ready(function () {
$("#edit-library-tooltip").tooltip();
// Javascript to enable link to tab
var hash = document.location.hash;
var prefix = "tab_";
if (hash) {
$('.user-info-nav a[href='+hash.replace(prefix,"")+']').tab('show').trigger('show.bs.tab');
}
// Change hash for page-reload
$('.user-info-nav a').on('shown.bs.tab', function (e) {
window.location.hash = e.target.hash.replace("#", "#" + prefix);
});
// Populate watch time stats
$.ajax({
@@ -389,237 +638,6 @@ DOCUMENTATION :: END
}
});
function loadHistoryTable() {
// Build watch history table
history_table_options.ajax = {
url: 'get_history',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
user_id: "${_session['user_id']}" == "None" ? null : "${_session['user_id']}"
};
}
}
history_table = $('#history_table-SID-${data["section_id"]}').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('#button-bar-history');
clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
}
$( "#history-tab-btn" ).one( "click", function() {
loadHistoryTable();
});
$('a[data-toggle="tab"]').on('shown.bs.tab', function (e) {
$.fn.dataTable.tables({ visible: true, api: true }).columns.adjust();
});
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
media_info_table_options.ajax = {
url: 'get_library_media_info',
type: 'post',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
section_id: section_id,
refresh: refresh_table
};
}
}
media_info_table = $('#media_info_table-SID-${data["section_id"]}').DataTable(media_info_table_options);
var colvis = new $.fn.dataTable.ColVis(media_info_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark' });
$(colvis.button()).appendTo('#button-bar-media-info');
clearSearchButton('media_info_table-SID-${data["section_id"]}', media_info_table);
}
$( "#media-info-tab-btn" ).one( "click", function() {
loadMediaInfoTable();
});
$("#refresh-media-info-table").click(function () {
media_info_child_table = {};
refresh_table = true;
refresh_child_tables = true;
media_info_table.draw();
refresh_table = false;
});
// Load edit library modal
$("#toggle-edit-library-modal").click(function() {
$("#edit-library-tooltip").tooltip('hide');
$.ajax({
url: 'edit_library_dialog',
data: { section_id: section_id },
cache: false,
async: true,
complete: function(xhr, status) {
$("#edit-library-modal").html(xhr.responseText);
}
});
});
$('#row-edit-mode').on('click', function() {
$('#row-edit-mode-alert').fadeIn(200);
if ($(this).hasClass('active')) {
if (history_to_delete.length > 0) {
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
});
history_table.draw();
});
}
$('.delete-control').each(function () {
$(this).addClass('hidden');
$('#row-edit-mode-alert').fadeOut(200);
});
} else {
history_to_delete = [];
$('.delete-control').each(function() {
$(this).find('button.btn-danger').toggleClass('btn-warning').toggleClass('btn-danger');
$(this).removeClass('hidden');
});
}
});
% endif
$("#refresh-history-list").click(function () {
history_table.draw();
});
function recentlyWatched() {
// Populate recently watched
$.ajax({
url: 'library_recently_watched',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-watched").html(xhr.responseText);
highlightWatchedScrollerButton();
}
});
}
function recentlyAdded() {
// Populate recently added
$.ajax({
url: 'library_recently_added',
async: true,
data: {
section_id: section_id,
limit: 50
},
complete: function(xhr, status) {
$("#library-recently-added").html(xhr.responseText);
highlightAddedScrollerButton();
}
});
}
recentlyWatched();
recentlyAdded();
function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-watched").width()) {
$("#recently-watched-page-right").removeClass("disabled");
} else {
$("#recently-watched-page-right").addClass("disabled");
}
}
function highlightAddedScrollerButton() {
var scroller = $("#recently-added-row-scroller");
var numElems = scroller.find("li").length;
scroller.width(numElems * 175);
if (scroller.width() > $("#library-recently-added").width()) {
$("#recently-added-page-right").removeClass("disabled");
} else {
$("#recently-added-page-right").addClass("disabled");
}
}
$(window).resize(function() {
highlightWatchedScrollerButton();
highlightAddedScrollerButton();
});
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
var leftTotalWatched = 0;
$(".paginate-watched").click(function (e) {
e.preventDefault();
var scroller = $("#recently-watched-row-scroller");
var containerWidth = $("#library-recently-watched").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalWatched = Math.max(Math.min(leftTotalWatched + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalWatched }, 250);
if (leftTotalWatched == 0) {
$("#recently-watched-page-left").addClass("disabled").blur();
} else {
$("#recently-watched-page-left").removeClass("disabled");
}
if (leftTotalWatched == leftMax) {
$("#recently-watched-page-right").addClass("disabled").blur();
} else {
$("#recently-watched-page-right").removeClass("disabled");
}
});
var leftTotalAdded = 0;
$(".paginate-added").click(function (e) {
e.preventDefault();
var scroller = $("#recently-added-row-scroller");
var containerWidth = $("#library-recently-added").width();
var scrollAmount = $(this).data("id") * parseInt(containerWidth / 175) * 175;
var leftMax = Math.min(-parseInt(scroller.width()) + Math.abs(scrollAmount), 0);
leftTotalAdded = Math.max(Math.min(leftTotalAdded + scrollAmount, 0), leftMax);
scroller.animate({ left: leftTotalAdded }, 250);
if (leftTotalAdded == 0) {
$("#recently-added-page-left").addClass("disabled").blur();
} else {
$("#recently-added-page-left").removeClass("disabled");
}
if (leftTotalAdded == leftMax) {
$("#recently-added-page-right").addClass("disabled").blur();
} else {
$("#recently-added-page-right").removeClass("disabled");
}
});
});
</script>
% endif

View File

@@ -29,14 +29,13 @@ DOCUMENTATION :: END
headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
'artist': ('Music Libraries', ('Artists', 'Albums', 'Tracks')),
'photo': ('Photo Libraries', ('Albums', '', 'Photos'))}
'photo': ('Photo Libraries', ('Albums', 'Photos', 'Videos'))}
%>
% for section_type in types:
% if section_type in data:
<div class="dashboard-stats-instance" id="library-stats-instance-${section_type}" data-section_type="${section_type}">
<div class="dashboard-stats-container">
<div class="dashboard-stats-background-overlay">
<div id="library-stats-background-${section_type}" class="dashboard-stats-background" style="background-image: url(pms_image_proxy?img=/:/resources/${section_type}-fanart.jpg&width=500&height=280&fallback=art);"></div>
<div id="library-stats-background-${section_type}" class="dashboard-stats-background" style="background-image: url(pms_image_proxy?img=/:/resources/${section_type}-fanart.jpg&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art);">
<div id="library-stats-thumb-${section_type}" class="dashboard-stats-flat svg-icon library-${section_type} hidden-xs"></div>
<div class="dashboard-stats-info-container">
<div id="library-stats-title-${section_type}" class="dashboard-stats-info-title">

View File

@@ -3,39 +3,32 @@
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tautulli - ${title} | ${server_name}</title>
<title>Tautulli - ${title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="${http_root}css/bootstrap3/bootstrap.css" rel="stylesheet">
<link href="${http_root}css/pnotify.custom.min.css" rel="stylesheet" />
<link href="${http_root}css/plexpy.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<link href="${http_root}css/opensans.min.css" rel="stylesheet">
<link href="${http_root}css/font-awesome.min.css" rel="stylesheet">
<!-- Favicons -->
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.0">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.0">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.0">
<link rel="icon" type="image/png" sizes="32x32" href="${http_root}images/favicon/favicon-32x32.png?v=2.0.5">
<link rel="icon" type="image/png" sizes="16x16" href="${http_root}images/favicon/favicon-16x16.png?v=2.0.5">
<link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS -->
<!-- Android >M39 icon -->
<link rel="icon" type="image/png" sizes="192x192" href="${http_root}images/favicon/android-chrome-192x192.png?v=2.0.0">
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0">
<meta name="theme-color" content="#1f1f1f">
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.0" color="#1f1f1f">
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
<link rel="mask-icon" href="${http_root}images/favicon/safari-pinned-tab.svg?v=2.0.5" color="#282a2d">
<meta name="apple-mobile-web-app-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="viewport" content="initial-scale=1">
<meta name="format-detection" content="telephone=no">
<!-- IE10 icon -->
<!-- Microsoft -->
<meta name="application-name" content="Tautulli">
<meta name="msapplication-TileColor" content="#1f1f1f">
<meta name="msapplication-TileImage" content="${http_root}images/favicon/mstile-144x144.png?v=2.0.0">
<meta name="msapplication-config" content="${http_root}xml/IEconfig.xml?v=2.0.0" />
<meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
</head>
<body>
@@ -44,21 +37,19 @@
<div class="row">
<div class="login-container">
<div class="login-logo">
<img alt="Tautulli" src="${http_root}images/logo-tautulli.png">
<img src="${http_root}images/logo-tautulli-100.png" height="100" alt="PlexPy">
</div>
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<form action="${http_root}auth/login" method="post">
% if msg:
<div class="alert alert-danger" style="text-align: center; padding: 8px;">
${msg}
<form id="login-form">
<div id="incorrect-login" class="alert alert-danger" style="text-align: center; padding: 8px; display: none;">
Incorrect username or password.
</div>
% endif
<div class="form-group">
<label for="username" class="control-label">
Username
</label>
<input type="text" id="username" name="username" class="form-control" autocorrect="off" autocapitalize="off" value="${username}" autofocus>
<input type="text" id="username" name="username" class="form-control" autocorrect="off" autocapitalize="off" autofocus>
</div>
<div class="form-group">
<label for="password" class="control-label">
@@ -72,7 +63,7 @@
<input type="checkbox" id="remember_me" name="remember_me" title="for 30 days" value="1" checked="checked" /> Remember me
</label>
</div>
<button type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
<button id="sign-in" type="submit" class="btn btn-bright login-button"><i class="fa fa-sign-in"></i>&nbsp; Sign In</button>
</div>
</form>
</div>
@@ -82,5 +73,30 @@
</div>
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script>
$('#login-form').submit(function(event) {
event.preventDefault();
$('#sign-in').prop('disabled', true).html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sign In');
$.ajax({
url: '${http_root}auth/signin',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
statusCode: {
200: function() {
window.location = "${redirect_uri or http_root}";
},
401: function() {
$('#incorrect-login').show();
$('#username').focus();
}
},
complete: function() {
$('#sign-in').prop('disabled', false).html('<i class="fa fa-sign-in"></i>&nbsp; Sign In');
}
});
});
</script>
</body>
</html>

View File

@@ -5,7 +5,7 @@
<%def name="headIncludes()">
<link rel="stylesheet" href="${http_root}css/dataTables.bootstrap.css">
<link rel="stylesheet" href="${http_root}css/plexpy-dataTables.css">
<link rel="stylesheet" href="${http_root}css/tautulli-dataTables.css">
<style>
td {word-break: break-all;}
</style>
@@ -21,9 +21,9 @@
<span><i class="fa fa-list-alt"></i> Logs</span>
</div>
<div class="button-bar">
<div class="btn-group" id="plexpy-log-levels">
<div class="btn-group" id="tautulli-log-levels">
<label>
<select name="plexpy-log-level-filter" id="plexpy-log-level-filter" class="btn" style="color: inherit;">
<select name="tautulli-log-level-filter" id="tautulli-log-level-filter" class="btn" style="color: inherit;">
<option value="">All log levels</option>
<option disabled>&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;&#9472;</option>
<option value="DEBUG">Debug</option>
@@ -45,28 +45,30 @@
</select>
</label>
</div>
<button class="btn btn-dark" id="download-plexpylog"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-tautullilog"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-plexserverlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="download-plexscannerlog" style="display: none;"><i class="fa fa-download"></i> Download logs</button>
<button class="btn btn-dark" id="clear-logs"><i class="fa fa-trash-o"></i> Clear logs</button>
<button class="btn btn-dark" id="clear-notify-logs" style="display: none;"><i class="fa fa-trash-o"></i> Clear logs</button>
<button class="btn btn-dark" id="clear-newsletter-logs" style="display: none;"><i class="fa fa-trash-o"></i> Clear logs</button>
<button class="btn btn-dark" id="clear-login-logs" style="display: none;"><i class="fa fa-trash-o"></i> Clear logs</button>
</div>
</div>
<div class='table-card-back'>
<div>
<ul id="log_tabs" class="nav nav-pills" role="tablist">
<li role="presentation" class="active"><a id="plexpy-logs-btn" href="#tabs-plexpy_log" aria-controls="tabs-plexpy_log" role="tab" data-toggle="tab">Tautulli Logs</a></li>
<li role="presentation"><a id="plexpy-api-logs-btn" href="#tabs-plexpy_api_log" aria-controls="tabs-plexpy_api_log" role="tab" data-toggle="tab">Tautulli API Logs</a></li>
<li role="presentation" class="active"><a id="tautulli-logs-btn" href="#tabs-tautulli_log" aria-controls="tabs-tautulli_log" role="tab" data-toggle="tab">Tautulli Logs</a></li>
<li role="presentation"><a id="tautulli-api-logs-btn" href="#tabs-tautulli_api_log" aria-controls="tabs-tautulli_api_log" role="tab" data-toggle="tab">Tautulli API Logs</a></li>
<li role="presentation"><a id="plex-logs-btn" href="#tabs-plex_log" aria-controls="tabs-plex_log" role="tab" data-toggle="tab">Plex Media Server Logs</a></li>
<li role="presentation"><a id="plex-scanner-logs-btn" href="#tabs-plex_scanner_log" aria-controls="tabs-plex_scanner_log" role="tab" data-toggle="tab">Plex Media Scanner Logs</a></li>
<li role="presentation"><a id="plexpy-websocket-logs-btn" href="#tabs-plex_websocket_log" aria-controls="tabs-plex_websocket_log" role="tab" data-toggle="tab">Plex Websocket Logs</a></li>
<li role="presentation"><a id="plex-websocket-logs-btn" href="#tabs-plex_websocket_log" aria-controls="tabs-plex_websocket_log" role="tab" data-toggle="tab">Plex Websocket Logs</a></li>
<li role="presentation"><a id="notification-logs-btn" href="#tabs-notification_log" aria-controls="tabs-notification_log" role="tab" data-toggle="tab">Notification Logs</a></li>
<li role="presentation"><a id="newsletter-logs-btn" href="#tabs-newsletter_log" aria-controls="tabs-newsletter_log" role="tab" data-toggle="tab">Newsletter Logs</a></li>
<li role="presentation"><a id="login-logs-btn" href="#tabs-login_log" aria-controls="tabs-login_log" role="tab" data-toggle="tab">Login Logs</a></li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-plexpy_log" data-logfile="plexpy">
<table class="display" id="plexpy_log_table" width="100%">
<div role="tabpanel" class="tab-pane active" id="tabs-tautulli_log" data-logfile="tautulli">
<table class="display" id="tautulli_log_table" width="100%">
<thead>
<tr>
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
@@ -77,8 +79,8 @@
<tbody></tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-plexpy_api_log" data-logfile="plexpy_api">
<table class="display" id="plexpy_api_log_table" width="100%">
<div role="tabpanel" class="tab-pane" id="tabs-tautulli_api_log" data-logfile="tautulli_api">
<table class="display" id="tautulli_api_log_table" width="100%">
<thead>
<tr>
<th class="min-tablet" align="left" id="timestamp">Timestamp</th>
@@ -141,6 +143,25 @@
<tbody></tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_log">
<table class="display" id="newsletter_log_table" width="100%">
<thead>
<tr>
<th align="left" id="newsletter_timestamp">Timestamp</th>
<th align="left" id="newsletter_newsletter_id">Newsletter ID</th>
<th align="left" id="newsletter_agent_name">Agent</th>
<th align="left" id="newsletter_notify_action">Action</th>
<th align="left" id="newsletter_subject_text">Subject Text</th>
<th align="left" id="newsletter_body_text">Body Text</th>
<th align="left" id="newsletter_start_date">Start Date</th>
<th align="left" id="newsletter_end_date">End Date</th>
<th align="left" id="newsletter_uuid">UUID</th>
<th align="left" id="newsletter_success"></th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-login_log">
<table class="display login_log_table" id="login_log_table" width="100%">
<thead>
@@ -191,12 +212,13 @@
<script src="${http_root}js/tables/logs.js${cache_param}"></script>
<script src="${http_root}js/tables/plex_logs.js${cache_param}"></script>
<script src="${http_root}js/tables/notification_logs.js${cache_param}"></script>
<script src="${http_root}js/tables/newsletter_logs.js${cache_param}"></script>
<script src="${http_root}js/tables/login_logs.js${cache_param}"></script>
<script>
$(document).ready(function() {
loadPlexPyLogs('plexpy', selected_log_level);
clearSearchButton('plexpy_log_table', log_table);
loadtautullilogs('tautulli', selected_log_level);
clearSearchButton('tautulli_log_table', log_table);
});
var log_levels = ['DEBUG', 'INFO', 'WARN', 'ERROR'];
@@ -227,10 +249,10 @@
}
var selected_log_level = null;
function loadPlexPyLogs(logfile, selected_log_level) {
function loadtautullilogs(logfile, selected_log_level) {
log_table_options.ajax = {
url: "get_log",
type: 'post',
url: 'get_log',
type: 'POST',
data: function (d) {
return {
logfile: logfile,
@@ -238,10 +260,10 @@
log_level: selected_log_level
};
}
}
};
log_table = $('#' + logfile + '_log_table').DataTable(log_table_options);
$('#plexpy-log-level-filter').on('change', function () {
$('#tautulli-log-level-filter').on('change', function () {
selected_log_level = $(this).val() || null;
log_table.draw();
});
@@ -249,131 +271,168 @@
function loadPlexLogs() {
plex_log_table_options.ajax = {
url: "get_plex_log?log_type=server"
}
url: 'get_plex_log?log_type=server',
type: 'POST'
};
plex_log_table_options.initComplete = bindLogLevelFilter;
plex_log_table = $('#plex_log_table').DataTable(plex_log_table_options);
}
function loadPlexScannerLogs() {
plex_log_table_options.ajax = {
url: "get_plex_log?log_type=scanner"
}
url: 'get_plex_log?log_type=scanner',
type: 'POST'
};
plex_log_table_options.initComplete = bindLogLevelFilter;
plex_scanner_log_table = $('#plex_scanner_log_table').DataTable(plex_log_table_options);
}
function loadNotificationLogs() {
notification_log_table_options.ajax = {
url: "get_notification_log",
url: 'get_notification_log',
type: 'POST',
data: function (d) {
return {
json_data: JSON.stringify(d)
};
}
}
};
notification_log_table = $('#notification_log_table').DataTable(notification_log_table_options);
}
function loadNewsletterLogs() {
newsletter_log_table_options.ajax = {
url: "get_newsletter_log",
data: function (d) {
return {
json_data: JSON.stringify(d)
};
}
};
newsletter_log_table = $('#newsletter_log_table').DataTable(newsletter_log_table_options);
}
function loadLoginLogs() {
login_log_table_options.pageLength = 50;
login_log_table_options.ajax = {
url: "get_user_logins",
url: 'get_user_logins',
type: 'POST',
data: function (d) {
return {
json_data: JSON.stringify(d)
};
}
}
};
login_log_table = $('#login_log_table').DataTable(login_log_table_options);
}
$("#plexpy-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#tautulli-logs-btn").click(function () {
$("#tautulli-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").show();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs('plexpy', selected_log_level);
clearSearchButton('plexpy_log_table', log_table);
loadtautullilogs('tautulli', selected_log_level);
clearSearchButton('tautulli_log_table', log_table);
});
$("#plexpy-api-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#tautulli-api-logs-btn").click(function () {
$("#tautulli-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").show();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs('plexpy_api', selected_log_level);
clearSearchButton('plexpy_api_log_table', log_table);
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").hide();
loadtautullilogs('tautulli_api', selected_log_level);
clearSearchButton('tautulli_api_log_table', log_table);
});
$("#plexpy-websocket-logs-btn").click(function () {
$("#plexpy-log-levels").show();
$("#plex-websocket-logs-btn").click(function () {
$("#tautulli-log-levels").show();
$("#plex-log-levels").hide();
$("#clear-logs").show();
$("#download-plexpylog").show()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").show();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").hide();
loadPlexPyLogs('plex_websocket', selected_log_level);
loadtautullilogs('plex_websocket', selected_log_level);
clearSearchButton('plex_websocket_log_table', log_table);
});
$("#plex-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").show();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").show()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").hide();
$("#download-plexserverlog").show();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").hide();
loadPlexLogs();
clearSearchButton('plex_log_table', plex_log_table);
});
$("#plex-scanner-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").show();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").show()
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").show();
$("#clear-notify-logs").hide();
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").hide();
loadPlexScannerLogs();
clearSearchButton('plex_scanner_log_table', plex_scanner_log_table);
});
$("#notification-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#tautulli-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").show();
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").hide();
loadNotificationLogs();
clearSearchButton('notification_log_table', notification_log_table);
});
$("#login-logs-btn").click(function () {
$("#plexpy-log-levels").hide();
$("#newsletter-logs-btn").click(function () {
$("#tautulli-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-plexpylog").hide()
$("#download-plexserverlog").hide()
$("#download-plexscannerlog").hide()
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-newsletter-logs").show();
$("#clear-login-logs").hide();
loadNewsletterLogs();
clearSearchButton('newsletter_log_table', newsletter_log_table);
});
$("#login-logs-btn").click(function () {
$("#tautulli-log-levels").hide();
$("#plex-log-levels").hide();
$("#clear-logs").hide();
$("#download-tautullilog").hide();
$("#download-plexserverlog").hide();
$("#download-plexscannerlog").hide();
$("#clear-notify-logs").hide();
$("#clear-newsletter-logs").hide();
$("#clear-login-logs").show();
loadLoginLogs();
clearSearchButton('login_log_table', notification_log_table);
@@ -384,9 +443,10 @@
});
$("#clear-logs").click(function () {
var logfile = $(".tab-pane.active").data('logfile')
var logfile = $(".tab-pane.active").data('logfile');
var title = $("#log_tabs li.active a").text();
$("#confirm-message").text("Are you sure you want to clear the Tautulli logs?");
$("#confirm-message").text("Are you sure you want to clear the " + title + "?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({
@@ -396,7 +456,7 @@
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -407,7 +467,7 @@
});
});
$("#download-plexpylog").click(function () {
$("#download-tautullilog").click(function () {
var logfile = $(".tab-pane.active").data('logfile');
window.location.href = "download_log?logfile=" + logfile;
});
@@ -421,7 +481,7 @@
});
$("#clear-notify-logs").click(function () {
$("#confirm-message").text("Are you sure you want to clear the Tautulli notification logs?");
$("#confirm-message").text("Are you sure you want to clear the Tautulli Notification Logs?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({
@@ -430,7 +490,7 @@
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -441,8 +501,29 @@
});
});
$("#clear-newsletter-logs").click(function () {
$("#confirm-message").text("Are you sure you want to clear the Tautulli Newsletter Logs?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({
url: 'delete_newsletter_log',
type: 'POST',
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
}
newsletter_log_table.draw();
}
});
});
});
$("#clear-login-logs").click(function () {
$("#confirm-message").text("Are you sure you want to clear the Tautulli login logs?");
$("#confirm-message").text("Are you sure you want to clear the Tautulli Login Logs?");
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
$.ajax({
@@ -451,7 +532,7 @@
complete: function (xhr, status) {
result = $.parseJSON(xhr.responseText);
msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -472,10 +553,10 @@
{
clearInterval(timer);
}
if(refreshrate.value != 0)
if(refreshrate.value !== 0)
{
timer = setInterval(function() {
if ($("#tabs-plexpy_log").hasClass("active") || $("#tabs-plexpy_api_log").hasClass("active") || $("#tabs-plex_websocket_log").hasClass("active")) {
if ($("#tabs-tautulli_log").hasClass("active") || $("#tabs-tautulli_api_log").hasClass("active") || $("#tabs-plex_websocket_log").hasClass("active")) {
log_table.ajax.reload();
} else if ($("#tabs-plex_log").hasClass("active")) {
plex_log_table.ajax.reload();

View File

@@ -55,7 +55,7 @@ DOCUMENTATION :: END
})
}
return deferred;
}
};
function checkQRAddress(url) {
var parser = document.createElement('a');
@@ -82,7 +82,7 @@ DOCUMENTATION :: END
verifiedDevice = false;
getPlexPyURL().then(function (url) {
checkQRAddress(url)
checkQRAddress(url);
$.get('generate_api_key', { device: true }).then(function (token) {
$('#api_qr_address').val(url);
@@ -120,7 +120,7 @@ DOCUMENTATION :: END
$('#api_qr_address').change(function () {
var url = $(this).val();
checkQRAddress(url)
checkQRAddress(url);
$('#api_qr_code').empty().qrcode({
text: url + '|' + $('#api_qr_token').val()

View File

@@ -0,0 +1,739 @@
% if newsletter:
<%!
import json
from plexpy import notifiers
from plexpy.helpers import anon_url, checked
all_notifiers = sorted(notifiers.get_notifiers(), key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id']))
email_notifiers = [n for n in all_notifiers if n['agent_name'] == 'email']
email_notifiers = [{'id': 0, 'agent_label': 'New Email Configuration', 'friendly_name': ''}] + email_notifiers
other_notifiers = [{'id': 0, 'agent_label': 'Select a Notification Agent', 'friendly_name': ''}] + all_notifiers
%>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title" id="newsletter-config-modal-header">${newsletter['agent_label']} Newsletter Settings &nbsp;<small><span class="newsletter_id">(Newsletter ID: ${newsletter['id']})</span></small></h4>
</div>
<div class="modal-body">
<div class="container-fluid">
<div class="row">
<ul class="nav nav-tabs list-unstyled" role="tablist">
<li role="presentation" class="active"><a href="#tabs-newsletter_config" aria-controls="tabs-newsletter_config" role="tab" data-toggle="tab">Configuration</a></li>
<li role="presentation"><a href="#tabs-newsletter_agent" aria-controls="tabs-newsletter_agent" role="tab" data-toggle="tab">Notification Agent</a></li>
<li role="presentation"><a href="#tabs-newsletter_text" aria-controls="tabs-newsletter_text" role="tab" data-toggle="tab">Newsletter Text</a></li>
<li role="presentation"><a href="#tabs-test_newsletter" aria-controls="tabs-test_newsletter" role="tab" data-toggle="tab">Test Newsletter</a></li>
</ul>
</div>
<form action="set_newsletter_config" method="post" class="form" id="set_newsletter_config" data-parsley-validate>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-newsletter_config">
<div class="row">
<div class="col-md-12">
<div class="checkbox" style="margin-bottom: 20px;">
<label>
<input type="checkbox" data-id="active_value" class="checkboxes" value="1" ${checked(newsletter['active'])}> Enable the Newsletter
</label>
<input type="hidden" id="active_value" name="active" value="${newsletter['active']}">
</div>
<div class="form-group">
<label for="custom_cron">Schedule</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="custom_cron" name="newsletter_config_custom_cron">
<option value="0" ${'selected' if newsletter['config']['custom_cron'] == 0 else ''}>Simple</option>
<option value="1" ${'selected' if newsletter['config']['custom_cron'] == 1 else ''}>Custom</option>
</select>
<input type="text" id="cron_value" name="cron" value="${newsletter['cron']}" />
<div id="cron-widget"></div>
</div>
</div>
<p class="help-block">
<span id="simple_cron_message">Set the schedule for the newsletter.</span>
<span id="custom_cron_message">Set the schedule for the newsletter using a <a href="${anon_url('https://crontab.guru')}" target="_blank">custom crontab</a>. Only standard cron values are valid.</span>
</p>
</div>
<div class="form-group">
<label for="time_frame">Time Frame</label>
<div class="row">
<div class="col-md-4">
<div class="input-group newsletter-time_frame">
<span class="input-group-addon form-control btn-dark inactive">Last</span>
<input type="number" class="form-control" id="newsletter_config_time_frame" name="newsletter_config_time_frame" value="${newsletter['config']['time_frame']}">
<select class="form-control" id="newsletter_config_time_frame_units" name="newsletter_config_time_frame_units">
<option value="days" ${'selected' if newsletter['config']['time_frame_units'] == 'days' else ''}>days</option>
<option value="hours" ${'selected' if newsletter['config']['time_frame_units'] == 'hours' else ''}>hours</option>
</select>
</div>
</div>
</div>
<p class="help-block">Set the time frame to include in the newsletter. Note: Days uses calendar days (i.e. since midnight).</p>
</div>
</div>
<div class="col-md-12" style="padding-top: 10px; border-top: 1px solid #444;">
<input type="hidden" id="newsletter_id" name="newsletter_id" value="${newsletter['id']}" />
<input type="hidden" id="agent_id" name="agent_id" value="${newsletter['agent_id']}" />
% for item in newsletter['config_options']:
% if item['input_type'] == 'help':
<div class="form-group">
<label>${item['label']}</label>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'text' or item['input_type'] == 'password':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'number':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-3">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'button':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</div>
</div>
<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" ${checked(item['value'])}> ${item['label']}
</label>
<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':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()):
% if key == item['value']:
<option value="${key}" selected>${value}</option>
% else:
<option value="${key}">${value}</option>
% endif
% endfor
</select>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'selectize':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
</optgroup>
% endfor
% else:
<option value="border-all"></option>
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
% endif
</select>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% endif
% endfor
</div>
<div class="col-md-12" style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #444;">
<div class="form-group">
<label for="friendly_name">Description</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${newsletter['friendly_name']}" size="30">
</div>
</div>
<p class="help-block">Optional: Enter a description to help identify this newsletter in the newsletters list.</p>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_agent">
<div class="row">
<div class="col-md-12">
<div class="checkbox">
<label>
<input type="checkbox" id="newsletter_config_formatted_checkbox" data-id="newsletter_config_formatted" class="checkboxes" value="1" ${checked(newsletter['config']['formatted'])}> Send newsletter as an HTML formatted Email
</label>
<p class="help-block">Enable to send the newsletter as an HTML formatted Email. Disable to only send a subject and body message to a different notification agent.</p>
<input type="hidden" id="newsletter_config_formatted" name="newsletter_config_formatted" value="${newsletter['config']['formatted']}">
</div>
<div class="form-group" id="email_notifier_select">
<label for="newsletter_email_notifier_id">Email Notification Agent</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="newsletter_email_notifier_id" name="newsletter_email_notifier_id">
% for notifier in email_notifiers:
<% selected = 'selected' if notifier['id'] == newsletter['email_config']['notifier_id'] else '' %>
% if notifier['friendly_name']:
<option value="${notifier['id']}" ${selected}>${notifier['agent_label']} (${notifier['id']} - ${notifier['friendly_name']})</option>
% elif notifier['id']:
<option value="${notifier['id']}" ${selected}>${notifier['agent_label']} (${notifier['id']})</option>
% else:
<option value="${notifier['id']}" ${selected}>${notifier['agent_label']}</option>
% endif
% endfor
</select>
</div>
</div>
<p class="help-block">
Select an existing Email notification agent or enter a new configuration below.<br>
Note: Make sure HTML support is enabled for the Email notification agent.
</p>
</div>
<div class="form-group" id="other_notifier_select">
<label for="newsletter_config_notifier_id">Notification Agent</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="newsletter_config_notifier_id" name="newsletter_config_notifier_id">
% for notifier in other_notifiers:
<% selected = 'selected' if notifier['id'] == newsletter['config']['notifier_id'] else '' %>
% if notifier['friendly_name']:
<option value="${notifier['id']}" ${selected}>${notifier['agent_label']} (${notifier['id']} - ${notifier['friendly_name']})</option>
% elif notifier['id']:
<option value="${notifier['id']}" ${selected}>${notifier['agent_label']} (${notifier['id']})</option>
% else:
<option value="${notifier['id']}" ${selected}>${notifier['agent_label']}</option>
% endif
% endfor
</select>
</div>
</div>
<p class="help-block">
Select an existing notification agent where the subject and body text will be sent.<br>
Note: Self-hosted newsletters must be enabled under <a data-tab-destination="tabs-notifications" data-dismiss="modal" data-target="#newsletter_self_hosted">Newsletters</a> to include a link to the newsletter.
</p>
</div>
</div>
<div id="newsletter-email-config" class="col-md-12" style="padding-top: 10px; border-top: 1px solid #444;">
% for item in newsletter['email_config_options']:
% if item['input_type'] == 'help':
<div class="form-group">
<label>${item['label']}</label>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'text' or item['input_type'] == 'password':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'number':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-3">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30">
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'button':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'checkbox' and item['name'] != 'newsletter_email_html_support':
<div class="checkbox">
<label>
<input type="checkbox" data-id="${item['name']}" class="checkboxes" value="1" ${checked(item['value'])}> ${item['label']}
</label>
<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':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()):
% if key == item['value']:
<option value="${key}" selected>${value}</option>
% else:
<option value="${key}">${value}</option>
% endif
% endfor
</select>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'selectize':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
</optgroup>
% endfor
% else:
<option value="border-all"></option>
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
% endif
</select>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% endif
% endfor
<input type="hidden" id="newsletter_email_html_support" name="newsletter_email_html_support" value="1">
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-newsletter_text">
<label>Newsletter Text</label>
<p class="help-block">
Set the custom formatted text for each type of notification.
<a href="#newsletter-text-sub-modal" data-toggle="modal">Click here</a> for a list of available parameters which can be used.
</p>
<p class="help-block">
You can also add text modifiers to change the case or slice parameters with a list of items.
<a href="#notify-text-modifiers-modal" data-toggle="modal">Click here</a> to view usage information.
</p>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="subject">Subject</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="subject" name="subject" value="${newsletter['subject']}" size="30">
</div>
</div>
<p class="help-block">
Enter a custom subject line for the newsletter. Leave blank for default.
</p>
</div>
<div class="form-group" id="newsletter_body">
<label for="body">Body</label>
<div class="row">
<div class="col-md-12">
<textarea class="form-control" id="body" name="body" data-autoresize>${newsletter['body']}</textarea>
</div>
</div>
<p class="help-block">
Enter a custom body line for the newsletter notification. Leave blank for default.
</p>
</div>
<div class="form-group">
<label for="message">Message</label>
<div class="row">
<div class="col-md-12">
<textarea class="form-control" id="message" name="message" data-autoresize>${newsletter['message']}</textarea>
</div>
</div>
<p class="help-block">
Enter a custom message to include on the newsletter.
</p>
</div>
</div>
</div>
</div>
<div role="tabpanel" class="tab-pane" id="tabs-test_newsletter">
<label>Preview Newsletter</label>
<p class="help-block">
Preview the ${newsletter['agent_label']} newsletter.
</p>
<div class="form-group">
<div class="row">
<div class="col-md-12">
<input type="button" class="btn btn-bright" id="preview_newsletter" name="preview_newsletter" value="Preview ${newsletter['agent_label']} Newsletter">
</div>
</div>
</div>
<label>Test Newsletter</label>
<p class="help-block">
Test if the ${newsletter['agent_label']} newsletter is working. Check the <a href="logs">logs</a> for troubleshooting.
</p>
<p class="help-block">
Warning: This will send an actual newsletter to your notification agent!
</p>
<div class="form-group">
<div class="row">
<div class="col-md-12">
<input type="button" class="btn btn-bright" id="test_newsletter" name="test_newsletter" value="Test ${newsletter['agent_label']} Newsletter">
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
<div class="modal-footer">
<input type="button" id="delete-newsletter-item" class="btn btn-danger btn-edit" style="float:left;" value="Delete">
<input type="button" id="duplicate-newsletter-item" class="btn btn-dark btn-edit" style="float:left;" value="Duplicate">
<input type="button" id="save-newsletter-item" class="btn btn-bright" value="Save">
</div>
</div>
</div>
<script src="${http_root}js/jquery-cron-min.js"></script>
<script>
$('#newsletter-config-modal').unbind('hidden.bs.modal');
var cron_widget = $('#cron-widget').cron({
initial: '0 0 * * 0',
classes: 'form-control cron-select',
onChange: function() {
$("#cron_value").val($(this).cron('value'));
}
});
if (${newsletter['config']['custom_cron']}) {
$('#cron_value').val('${newsletter['cron']}');
} else {
try {
cron_widget.cron('value', '${newsletter['cron']}');
} catch (e) {}
}
function toggleCustomCron() {
if ($('#custom_cron').val() === '1'){
$('#cron-widget').hide();
$('#cron_value').show();
$('#simple_cron_message').hide();
$('#custom_cron_message').show();
} else {
$('#cron-widget').show();
$('#cron_value').hide();
$('#simple_cron_message').show();
$('#custom_cron_message').hide();
}
}
toggleCustomCron();
$('#custom_cron').change(function () {
toggleCustomCron();
});
var $incl_libraries = $('#newsletter_config_incl_libraries').selectize({
plugins: ['remove_button'],
maxItems: null,
render: {
option: function(item) {
if (item.value.endsWith('-all')) {
return '<div class="' + item.value + '">' + item.text + '</div>'
}
return '<div>' + item.text + '</div>';
}
},
onItemAdd: function(value) {
if (value === 'select-all') {
var all_keys = $.map(this.options, function(option){
return option.value.endsWith('-all') ? null : option.value;
});
this.setValue(all_keys);
} else if (value === 'remove-all') {
this.clear();
this.refreshOptions();
this.positionDropdown();
}
}
});
var incl_libraries = $incl_libraries[0].selectize;
incl_libraries.setValue(${json.dumps(next((c['value'] for c in newsletter['config_options'] if c['name'] == 'newsletter_config_incl_libraries'), [])) | n});
function toggleEmailSelect () {
if ($('#newsletter_config_formatted_checkbox').is(':checked')) {
$('#newsletter_body').hide();
$('#email_notifier_select').show();
$('#other_notifier_select').hide();
toggleNewEmailConfig();
} else {
$('#newsletter_body').show();
$('#email_notifier_select').hide();
$('#other_notifier_select').show();
$('#newsletter-email-config').hide();
}
}
toggleEmailSelect();
$('#newsletter_config_formatted_checkbox').change(function () {
toggleEmailSelect();
});
function toggleNewEmailConfig () {
if ($('#newsletter_config_formatted_checkbox').is(':checked') && $('#newsletter_email_notifier_id').val() === '0') {
$('#newsletter-email-config').show();
} else {
$('#newsletter-email-config').hide();
}
}
toggleNewEmailConfig();
$('#newsletter_email_notifier_id').change(function () {
toggleNewEmailConfig();
});
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)';
var $email_selectors = $('#newsletter_email_to, #newsletter_email_cc, #newsletter_email_bcc').selectize({
plugins: ['remove_button'],
maxItems: null,
render: {
item: function(item, escape) {
return '<div>' +
(item.text ? '<span class="item-text">' + escape(item.text) + '</span>' : '') +
(item.value ? '<span class="item-value">' + escape(item.value) + '</span>' : '') +
'</div>';
},
option: function(item, escape) {
var label = item.text || item.value;
var caption = item.text ? item.value : null;
if (item.value.endsWith('-all')) {
return '<div class="' + item.value + '">' + escape(label) + '</div>'
}
return '<div>' +
escape(label) +
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
'</div>';
}
},
onItemAdd: function(value) {
if (value === 'select-all') {
var all_keys = $.map(this.options, function(option){
return option.value.endsWith('-all') ? null : option.value;
});
this.setValue(all_keys);
} else if (value === 'remove-all') {
this.clear();
this.refreshOptions();
this.positionDropdown();
}
},
createFilter: function(input) {
var match, regex;
// email@address.com
regex = new RegExp('^' + REGEX_EMAIL + '$', 'i');
match = input.match(regex);
if (match) return !this.options.hasOwnProperty(match[0]);
// user <email@address.com>
regex = new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i');
match = input.match(regex);
if (match) return !this.options.hasOwnProperty(match[2]);
return false;
},
create: function(input) {
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
return {value: input};
}
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
if (match) {
return {
value : match[2],
text : $.trim(match[1])
};
}
return false;
}
});
var email_to = $email_selectors[0].selectize;
var email_cc = $email_selectors[1].selectize;
var email_bcc = $email_selectors[2].selectize;
email_to.setValue(${json.dumps(next((c['value'] for c in newsletter['email_config_options'] if c['name'] == 'newsletter_email_to'), [])) | n});
email_cc.setValue(${json.dumps(next((c['value'] for c in newsletter['email_config_options'] if c['name'] == 'newsletter_email_cc'), [])) | n});
email_bcc.setValue(${json.dumps(next((c['value'] for c in newsletter['email_config_options'] if c['name'] == 'newsletter_email_bcc'), [])) | n});
function reloadModal() {
$.ajax({
url: 'get_newsletter_config_modal',
data: { newsletter_id: '${newsletter["id"]}' },
cache: false,
async: true,
complete: function (xhr, status) {
$('#newsletter-config-modal').html(xhr.responseText);
}
});
}
function saveCallback(jqXHR) {
if (jqXHR) {
var result = $.parseJSON(jqXHR.responseText);
var msg = result.message;
if (result.result == 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
}
}
getNewslettersTable();
}
function deleteCallback() {
$('#newsletter-config-modal').modal('hide');
getNewslettersTable();
}
function duplicateCallback(result) {
// Set new newsletter id
$('#newsletter_id').val(result.newsletter_id);
// Clear friendly name
$('#friendly_name').val("");
saveNewsletter();
$('#newsletter-config-modal').on('hidden.bs.modal', function () {
loadNewsletterConfig(result.newsletter_id);
});
$('#newsletter-config-modal').modal('hide');
}
function saveNewsletter() {
// Trim all text inputs before saving
$('input[type=text]').val(function(_, value) {
return $.trim(value);
});
// Make sure simple cron value is set
if ($('#custom_cron').val() === '0'){
$("#cron_value").val(cron_widget.cron('value'));
}
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, true, saveCallback);
}
$('#delete-newsletter-item').click(function () {
var msg = 'Are you sure you want to delete this <strong>${newsletter["agent_label"]}</strong> newsletter?';
var url = 'delete_newsletter';
confirmAjaxCall(url, msg, { newsletter_id: '${newsletter["id"]}' }, null, deleteCallback);
});
$('#duplicate-newsletter-item').click(function() {
var msg = 'Are you sure you want to duplicate this <strong>${newsletter["agent_label"]}</strong> newsletter?';
var url = 'add_newsletter_config';
confirmAjaxCall(url, msg, { agent_id: '${newsletter["agent_id"]}' }, null, duplicateCallback);
});
$('#save-newsletter-item').click(function () {
saveNewsletter();
});
$('#preview_newsletter').click(function () {
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, false, previewNewsletter);
});
$('#test_newsletter').click(function () {
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, false, sendTestNewsletter);
});
function previewNewsletter() {
showMsg('<i class="fa fa-check"></i>&nbsp; Check pop-up blocker if no response.', false, true, 2000);
window.open('newsletter_preview?newsletter_id=' + $('#newsletter_id').val());
}
function sendTestNewsletter() {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sending Newsletter', false);
$.ajax({
url: 'send_newsletter',
data: {
newsletter_id: $('#newsletter_id').val(),
notify_action: 'test'
},
cache: false,
async: true,
success: function (data) {
if (data.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
} else {
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
}
}
});
}
$("${', '.join(['#' + c['name'] for c in newsletter['config_options'] if c.get('refresh')])}").on('change', function () {
// Reload modal to update certain fields
doAjaxCall('set_newsletter_config', $(this), 'tabs', true, false, reloadModal);
return false;
});
// Never send checkbox values directly, always substitute value in hidden input.
$('.checkboxes').click(function () {
var configToggle = $(this).data('id');
if ($(this).is(':checked')) {
$('#'+configToggle).val(1);
} else {
$('#'+configToggle).val(0);
}
});
// 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>
% else:
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title" id="newsletter-config-modal-header">Error</h4>
</div>
<div class="modal-body" style="text-align: center">
<strong>
<i class="fa fa-exclamation-circle"></i> Failed to retrieve newsletter configuration. Check the <a href="logs">logs</a> for more info.
</strong>
</div>
<div class="modal-footer">
</div>
</div>
</div>
% endif

View File

@@ -0,0 +1,48 @@
<%
import urllib
%>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Tautulli - ${title} | ${server_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<style>
* {
padding: 0 !important;
margin: 0 !important;
}
</style>
</head>
<body>
<div id="loader" class="newsletter-loader-container">
<div class="newsletter-loader-message">
<div class="newsletter-loader"></div>
<br>
Generating Newsletter
<br>
Please wait, this may take a few minutes...
</div>
</div>
<script src="${http_root}js/jquery-2.1.4.min.js"></script>
<script>
$(document).ready(function () {
var frame = $('<iframe></iframe>', {
src: '${http_root}real_newsletter?${urllib.urlencode(kwargs) | n}',
frameborder: '0',
style: 'display: none; height: 100vh; width: 100vw;'
});
frame.on('load', function (e) {
$(e.target).fadeIn();
$('#loader').fadeOut();
});
$('body').append(frame);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
<%doc>
USAGE DOCUMENTATION :: PLEASE LEAVE THIS AT THE TOP OF THIS FILE
For Mako templating syntax documentation please visit: http://docs.makotemplates.org/en/latest/
Filename: newsletters_table.html
Version: 0.1
DOCUMENTATION :: END
</%doc>
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %>
<ul class="stacked-configs list-unstyled">
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
<li class="newsletter-agent" data-id="${newsletter['id']}">
<span>
<span class="toggle-left trigger-tooltip ${'active' if newsletter['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Newsletter ${'active' if newsletter['active'] else 'inactive'}"><i class="fa fa-lg fa-newspaper-o"></i></span>
% if newsletter['friendly_name']:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']} - ${newsletter['friendly_name']})</span>
% else:
${newsletter['agent_label']} &nbsp;<span class="friendly_name">(${newsletter['id']})</span>
% endif
<span class="toggle-right"><i class="fa fa-lg fa-cog"></i></span>
<span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}">
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
<script>
$("#newsletter-next_run-${newsletter['id']}").text(moment("${job.next_run_time}", "YYYY-MM-DD HH:mm:ssZ").fromNow())
</script>
% endif
</span>
</span>
</li>
% endfor
<li class="add-newsletter-agent" id="add-newsletter-agent" data-target="#add-newsletter-modal" data-toggle="modal">
<span>
<span class="toggle-left"><i class="fa fa-lg fa-newspaper-o"></i></span> Add a new newsletter agent
<span class="toggle-right"><i class="fa fa-lg fa-plus"></i></span>
</span>
</li>
</ul>
<script>
// Load newsletter config modal
$(".newsletter-agent").click(function () {
var newsletter_id = $(this).data('id');
loadNewsletterConfig(newsletter_id);
});
$('.trigger-tooltip').tooltip();
</script>

View File

@@ -1,10 +1,12 @@
<%!
from plexpy import helpers, notifiers
available_notification_actions = notifiers.available_notification_actions()
%>
% if notifier:
<link href="${http_root}css/selectize.bootstrap3.css" rel="stylesheet" />
<link href="${http_root}css/selectize.min.css" rel="stylesheet" />
<%!
import json
from plexpy import helpers, notifiers, users
available_notification_actions = notifiers.available_notification_actions()
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']]
sorted(user_emails, key=lambda u: u['user'])
%>
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
@@ -15,7 +17,7 @@
<div class="container-fluid">
<div class="row">
<ul class="nav nav-tabs list-unstyled" role="tablist">
<li role="presentation" class="active"><a href="#tabs-config" aria-controls="tabs-config" role="tab" data-toggle="tab">Configuration</a></li>
<li role="presentation" class="active"><a href="#tabs-notifier_config" aria-controls="tabs-notifier_config" role="tab" data-toggle="tab">Configuration</a></li>
<li role="presentation"><a href="#tabs-notify_triggers" aria-controls="tabs-notify_triggers" role="tab" data-toggle="tab">Triggers</a></li>
<li role="presentation"><a href="#tabs-notify_conditions" aria-controls="tabs-notify_conditions" role="tab" data-toggle="tab">Conditions</a></li>
<li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">${'Arguments' if notifier['agent_name'] == 'scripts' else 'Text'}</a></li>
@@ -24,7 +26,7 @@
</div>
<form action="set_notifier_config" method="post" class="form" id="set_notifier_config" data-parsley-validate>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="tabs-config">
<div role="tabpanel" class="tab-pane active" id="tabs-notifier_config">
<div class="row">
<div class="col-md-12">
<input type="hidden" id="notifier_id" name="notifier_id" value="${notifier['id']}" />
@@ -39,11 +41,8 @@
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<input type="${item['input_type']}" class="form-control" id="${item['name']}" name="${item['name']}" value="${item['value']}" size="30" ${'readonly' if item.get('readonly') else ''}>
% if item['name'] == 'osx_notify_app':
<a href="javascript:void(0)" id="osxnotifyregister">Register</a>
% endif
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
@@ -62,7 +61,7 @@
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<input type="button" class="btn btn-bright" id="${item['name']}" name="${item['name']}" value="${item['value']}">
</div>
</div>
@@ -80,7 +79,7 @@
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()):
% if key == item['value']:
@@ -94,6 +93,33 @@
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% elif item['input_type'] == 'selectize':
<div class="form-group">
<label for="${item['name']}">${item['label']}</label>
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
</optgroup>
% endfor
% else:
<option value="border-all"></option>
% for option in sorted(item['select_options'], key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
% endfor
% endif
</select>
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
</div>
% endif
% endfor
</div>
@@ -101,7 +127,7 @@
<div class="form-group">
<label for="friendly_name">Description</label>
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<input type="text" class="form-control" id="friendly_name" name="friendly_name" value="${notifier['friendly_name']}" size="30">
</div>
</div>
@@ -120,7 +146,7 @@
% for action in available_notification_actions:
<div class="checkbox">
<label>
<input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${helpers.checked(notifier['actions'][action['name']])}> Notify on ${action['label']}
<input type="checkbox" data-id="${action['name']}" class="checkboxes" value="1" ${helpers.checked(notifier['actions'][action['name']])}> ${action['label']}
</label>
<p class="help-block">${action['description'] | n}</p>
<input type="hidden" id="${action['name']}" name="${action['name']}" value="${notifier['actions'][action['name']]}">
@@ -132,20 +158,19 @@
<div role="tabpanel" class="tab-pane" id="tabs-notify_conditions">
<label>Notification Conditions</label>
<p class="help-block">
Add custom notification conditions.
Add custom conditions to only <em>allow certain notifications</em>. By default, all notifications will be sent if there are no conditions.
<a href="#notify-text-sub-modal" data-toggle="modal">Click here</a> for a description of all the parameters.
</p>
<p class="help-block">
Note: Conditions are checked after the notification trigger and the notification will only be sent if the condition logic is satisfied.
</p>
<div id="condition-widget"></div>
<input type="hidden" name="custom_conditions" id="custom_conditions" />
<input type="hidden" id="custom_conditions" name="custom_conditions" />
<div class="form-group">
<label for="custom_condition_logic">Condition Logic</label>
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" required />
<label for="custom_conditions_logic">Condition Logic</label>
<input type="text" class="form-control" name="custom_conditions_logic" id="custom_conditions_logic" value="${notifier['custom_conditions_logic']}" />
<div id="custom_conditions_logic_error" class="alert alert-danger" role="alert" style="padding-top: 5px; padding-bottom: 5px; margin: 0; display: none;"><i class="fa fa-exclamation-triangle" style="color: #a94442;"></i> <span></span></div>
<p class="help-block">
Enter the logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
Optional: Enter custom logic to use when evaluating the conditions (e.g. <span class="inline-pre">{1} and ({2} or {3})</span>).
Leave blank for implicit <span class="inline-pre">and</span> between all conditions.
</p>
<p class="help-block">
Note: Only the keywords <span class="inline-pre">and</span>/<span class="inline-pre">or</span> and brackets <span class="inline-pre">()</span> are supported.
@@ -186,7 +211,7 @@
</div>
<div class="form-group">
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<input type="button" class="btn btn-bright notifier-text-preview" data-action="${action['name']}" value="Preview Arguments">
</div>
</div>
@@ -213,7 +238,7 @@
</div>
<div class="form-group">
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<input type="button" class="btn btn-bright notifier-text-preview" data-action="${action['name']}" value="Preview Text">
</div>
</div>
@@ -279,7 +304,7 @@
% endif
<div class="form-group">
<div class="row">
<div class="col-md-8">
<div class="col-md-12">
<input type="button" class="btn btn-bright" id="test_notifier" name="test_notifier" value="Test ${notifier['agent_label']}">
</div>
</div>
@@ -303,16 +328,16 @@
$('#notifier-config-modal').unbind('hidden.bs.modal');
// Need this for setting conditions since conditions contain the character "
$('#custom_conditions').val('${notifier['custom_conditions'] | n}')
$('#custom_conditions').val(JSON.stringify(${json.dumps(notifier["custom_conditions"]) | n}));
$('#condition-widget').filterer({
parameters: ${parameters | n},
conditions: ${notifier["custom_conditions"] | n},
parameters: ${json.dumps(parameters) | n},
conditions: ${json.dumps(notifier["custom_conditions"]) | n},
updateConditions: function(newConditions){
$('#custom_conditions').val(JSON.stringify(newConditions));
}
})
});
function reloadModal() {
$.ajax({
url: 'get_notifier_config_modal',
@@ -329,7 +354,7 @@
if (jqXHR) {
var result = $.parseJSON(jqXHR.responseText);
var msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000)
} else {
showMsg('<i class="fa fa-times"></i> ' + msg, false, true, 5000, true)
@@ -365,8 +390,10 @@
$('input[type=text]').val(function(_, value) {
return $.trim(value);
});
// Reload modal to update certain fields
doAjaxCall('set_notifier_config', $(this), 'tabs', true, true, saveCallback);
if (validateLogic()) {
// Reload modal to update certain fields
doAjaxCall('set_notifier_config', $(this), 'tabs', true, true, saveCallback);
}
}
$('#delete-notifier-item').click(function () {
@@ -378,7 +405,7 @@
$('#duplicate-notifier-item').click(function() {
var msg = 'Are you sure you want to duplicate this <strong>${notifier["agent_label"]}</strong> notification agent?';
var url = 'add_notifier_config';
confirmAjaxCall(url, msg, { agent_id: "${notifier['agent_id']}" }, null, duplicateCallback);
confirmAjaxCall(url, msg, { agent_id: '${notifier["agent_id"]}' }, null, duplicateCallback);
});
$('#save-notifier-item').click(function () {
@@ -386,34 +413,49 @@
});
% if notifier['agent_name'] == 'facebook':
if (location.protocol !== 'https:') {
$('#tabs-config .form-group:first').prepend(
'<div class="form-group">' +
'<label>Warning</label>' +
'<p class="help-block" style="color: #eb8600;">Facebook requires HTTPS for authorization. ' +
'Please enable HTTPS for Tautulli under <a data-tab-destination="tabs-web_interface" data-dismiss="modal" data-target="#enable_https">Web Interface</a>.</p>' +
'</div>'
);
$('#facebook_redirect_uri').val('HTTPS not enabled');
} else {
$('#facebook_redirect_uri').val(location.href.split('/settings')[0] + '/facebook_redirect');
}
function disableFacebookRequest() {
if ($('#facebook_app_id').val() != '' && $('#facebook_app_secret').val() != '') { $('#facebook_facebookStep1').prop('disabled', false); }
else { $('#facebook_facebookStep1').prop('disabled', true); }
if ($('#facebook_app_id').val() !== '' && $('#facebook_app_secret').val() !== '') { $('#facebook_facebook_auth').prop('disabled', false); }
else { $('#facebook_facebook_auth').prop('disabled', true); }
}
disableFacebookRequest();
$('#facebook_app_id, #facebook_app_secret').on('change', function () {
disableFacebookRequest();
});
$('#facebook_facebookStep1').click(function () {
$('#facebook_facebook_auth').click(function () {
// Remove trailing '/' from Facebook redirect URI
if ($('#facebook_redirect_uri') && $('#facebook_redirect_uri').val().endsWith('/')) {
$('#facebook_redirect_uri').val($('#facebook_redirect_uri').val().slice(0, -1));
}
var facebook_token;
$.ajax({
url: 'facebookStep1',
url: 'facebook_auth',
data: {
app_id: $('#facebook_app_id').val(),
app_secret: $('#facebook_app_secret').val(),
redirect_uri: $('#facebook_redirect_uri').val(),
redirect_uri: $('#facebook_redirect_uri').val()
},
cache: false,
async: true,
complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = result.msg;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 5000);
window.open(result.url);
@@ -452,20 +494,231 @@
$('#notifier-config-modal').on('hidden.bs.modal', function () {
facebook_token = false;
})
});
% elif notifier['agent_name'] == 'browser':
$('#browser_allow_browser').click(function () {
PNotify.desktop.permission();
})
});
% elif notifier['agent_name'] == 'osx':
$('#osxnotifyregister').click(function () {
$('#osx_notify_register').click(function () {
var osx_notify_app = $('#osx_notify_app').val();
$.get('osxnotifyregister', { 'app': osx_notify_app }, function (data) { showMsg('<i class="fa fa-check"></i> ' + data, false, true, 3000); });
})
});
% elif notifier['agent_name'] == 'email':
var REGEX_EMAIL = '([a-z0-9!#$%&\'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&\'*+/=?^_`{|}~-]+)*@' +
'(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?)';
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
plugins: ['remove_button'],
maxItems: null,
render: {
item: function(item, escape) {
return '<div>' +
(item.text ? '<span class="item-text">' + escape(item.text) + '</span>' : '') +
(item.value ? '<span class="item-value">' + escape(item.value) + '</span>' : '') +
'</div>';
},
option: function(item, escape) {
var label = item.text || item.value;
var caption = item.text ? item.value : null;
if (item.value.endsWith('-all')) {
return '<div class="' + item.value + '">' + escape(label) + '</div>'
}
return '<div>' +
escape(label) +
(caption ? '<span class="caption">' + escape(caption) + '</span>' : '') +
'</div>';
}
},
onItemAdd: function(value) {
if (value === 'select-all') {
var all_keys = $.map(this.options, function(option){
return option.value.endsWith('-all') ? null : option.value;
});
this.setValue(all_keys);
} else if (value === 'remove-all') {
this.clear();
this.refreshOptions();
this.positionDropdown();
}
},
createFilter: function(input) {
var match, regex;
// email@address.com
regex = new RegExp('^' + REGEX_EMAIL + '$', 'i');
match = input.match(regex);
if (match) return !this.options.hasOwnProperty(match[0]);
// user <email@address.com>
regex = new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i');
match = input.match(regex);
if (match) return !this.options.hasOwnProperty(match[2]);
return false;
},
create: function(input) {
if ((new RegExp('^' + REGEX_EMAIL + '$', 'i')).test(input)) {
return {value: input};
}
var match = input.match(new RegExp('^([^<]*)\<' + REGEX_EMAIL + '\>$', 'i'));
if (match) {
return {
value : match[2],
text : $.trim(match[1])
};
}
return false;
}
});
var email_to = $email_selectors[0].selectize;
var email_cc = $email_selectors[1].selectize;
var email_bcc = $email_selectors[2].selectize;
email_to.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'email_to'), [])) | n});
email_cc.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'email_cc'), [])) | n});
email_bcc.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'email_bcc'), [])) | n});
% elif notifier['agent_name'] == 'join':
var $join_device_names = $('#join_device_names').selectize({
plugins: ['remove_button'],
maxItems: null,
create: true
});
var join_device_names = $join_device_names[0].selectize;
join_device_names.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'join_device_names'), [])) | n});
% elif notifier['agent_name'] == 'zapier':
$('#zapier_test_hook').click(function () {
$.get('zapier_test_hook', { 'zapier_hook': $('#zapier_hook').val() }, function (data) {
if (data.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + data.msg, false, true, 5000);
} else {
showMsg('<i class="fa fa-times"></i> ' + data.msg, false, true, 5000, true);
}
});
});
% elif notifier['agent_name'] == 'pushover':
function pushoverPriority() {
if ($('#pushover_priority').val() == '2') {
$('#pushover_retry').closest('.form-group').show();
$('#pushover_expire').closest('.form-group').show();
} else {
$('#pushover_retry').closest('.form-group').hide();
$('#pushover_expire').closest('.form-group').hide();
}
}
pushoverPriority();
$('#pushover_priority').change( function () {
pushoverPriority();
});
% endif
function validateLogic() {
const valid_tokens = /(\(|\)|and|or)/g;
const conditions_pattern = /{\d+}/g;
var custom_conditions = $.parseJSON($('#custom_conditions').val());
var custom_conditions_logic = $('#custom_conditions_logic').val();
var num_cond = custom_conditions.length;
var tokens = $.map(custom_conditions_logic.toLowerCase().split(valid_tokens), $.trim).filter(String);
var stack = [[]];
var temp;
var cond_next = true;
var bool_next = false;
var open_bracket_next = true;
var close_bracket_next = false;
var nest_and = 0;
var nest_nest_and = 0;
try {
$.each(tokens, function(i, x) {
if (open_bracket_next && x === '(') {
stack[stack.length-1].push([]);
temp = stack[stack.length-1];
stack.push(temp[temp.length-1]);
cond_next = true;
bool_next = false;
open_bracket_next = true;
close_bracket_next = false;
if (nest_and) {
nest_nest_and += 1
}
} else if (close_bracket_next && x === ')') {
stack.pop();
if (stack.length === 0) {
throw 'opening bracket is missing';
}
cond_next = false;
bool_next = true;
open_bracket_next = false;
close_bracket_next = true;
if (nest_and > 0 && nest_nest_and > 0 && nest_and === nest_nest_and) {
stack.pop();
nest_and -= 1;
nest_nest_and -= 1;
}
} else if (cond_next && x.match(conditions_pattern)) {
if (isNaN(x.slice(1, -1))) {
throw 'invalid condition logic'
} else {
var num = parseInt(x.slice(1, -1));
}
if (!(0 < num && num <= num_cond)) {
throw 'invalid condition number in condition logic'
}
stack[stack.length-1].push(num);
cond_next = false;
bool_next = true;
open_bracket_next = false;
close_bracket_next = true;
if (nest_and > nest_nest_and) {
stack.pop();
nest_and -= 1;
}
} else if (bool_next && x === 'and' && i < tokens.length-1) {
stack[stack.length-1].push([]);
temp = stack[stack.length-1];
stack.push(temp[temp.length-1]);
temp = stack[stack.length-2];
stack[stack.length-1].push(temp.splice(0, temp.length-2) + temp.splice(temp.length-2, temp.length-1));
stack[stack.length-1].push(x);
cond_next = true;
bool_next = false;
open_bracket_next = true;
close_bracket_next = false;
nest_and += 1;
} else if (bool_next && x === 'or' && i < tokens.length-1) {
stack[stack.length-1].push(x);
cond_next = true;
bool_next = false;
open_bracket_next = true;
close_bracket_next = false;
} else {
throw 'invalid condition logic';
}
});
if (stack.length > 1) {
throw 'closing bracket is missing';
}
$('#custom_conditions_logic_error').hide();
return true;
} catch (e) {
$('#custom_conditions_logic_error span').text(e);
$('#custom_conditions_logic_error').show();
showMsg('<i class="fa fa-times"></i> Failed to save notifier. Invalid condition logic.', false, true, 5000, true);
}
}
$('.notifier-text-preview').click(function () {
var action = $(this).data('action');
var subject = $('#' + action + '_subject').val();
@@ -493,11 +746,12 @@
});
function sendTestNotification() {
if ('${notifier["agent_name"]}' != 'browser') {
showMsg('<i class="fa fa-refresh fa-spin"></i>&nbsp; Sending Notification', false);
if ('${notifier["agent_name"]}' !== 'browser') {
$.ajax({
url: 'send_notification',
data: {
notifier_id: '${notifier["id"]}',
notifier_id: $('#notifier_id').val(),
subject: $('#test_subject').val(),
body: $('#test_body').val(),
script: $('#test_script').val(),
@@ -506,18 +760,16 @@
},
cache: false,
async: true,
complete: function (xhr, status) {
if (xhr.responseText.indexOf('sent') > -1) {
msg = '<i class="fa fa-check"></i>&nbsp; ' + xhr.responseText;
showMsg(msg, false, true, 2000);
success: function (data) {
if (data.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + data.message, false, true, 5000);
} else {
msg = '<i class="fa fa-times"></i>&nbsp; ' + xhr.responseText;
showMsg(msg, false, true, 2000, true);
showMsg('<i class="fa fa-exclamation-circle"></i> ' + data.message, false, true, 5000, true);
}
}
});
} else {
if ($('#browser_auto_hide_delay').val() == "0") {
if ($('#browser_auto_hide_delay').val() === "0") {
PNotify.prototype.options.hide = false;
} else {
PNotify.prototype.options.hide = true;
@@ -564,10 +816,10 @@
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title" id="notifier-config-modal-header">Error</h4>
</div>
<div class="modal-body">
<center><strong>
<div class="modal-body" style="text-align: center">
<strong>
<i class="fa fa-exclamation-circle"></i> Failed to retrieve notifier configuration. Check the <a href="logs">logs</a> for more info.
</strong></center>
</strong>
</div>
<div class="modal-footer">
</div>

View File

@@ -10,7 +10,7 @@ DOCUMENTATION :: END
</%doc>
<ul class="stacked-configs list-unstyled">
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
% for notifier in sorted(notifiers_list, key=lambda k: (k['agent_label'].lower(), k['friendly_name'], k['id'])):
<li class="notification-agent" data-id="${notifier['id']}">
<span>
<span class="toggle-left trigger-tooltip ${'active' if notifier['active'] else ''}" data-toggle="tooltip" data-placement="top" title="Triggers ${'active' if notifier['active'] else 'inactive'}"><i class="fa fa-lg fa-bell"></i></span>

View File

@@ -63,7 +63,7 @@ DOCUMENTATION :: END
<h3 class="text-muted">&nbsp;</h3>
</div>
% elif item['media_type'] == 'show':
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}">
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">
<div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=450&fallback=poster);">
<div class="dashboard-recent-media-overlay">

Some files were not shown because too many files have changed in this diff Show More