Compare commits

...

940 Commits

Author SHA1 Message Date
JonnyWong16
14b98a32e0 v2.5.6 2020-10-02 20:35:06 -07:00
JonnyWong16
a985cec9c2 Fix loading synced items for guest access 2020-10-02 11:16:49 -07:00
JonnyWong16
5dc0d5536d Also add username hover to most active card 2020-09-29 21:00:47 -07:00
JonnyWong16
e3eca5af46 Change friendly name hover title text to username 2020-09-29 20:07:23 -07:00
JonnyWong16
d9ece291b7 Fix 1px off dropdown menus 2020-09-28 18:23:07 -07:00
JonnyWong16
221d6e136a Added remote access down notification threshold setting 2020-09-27 19:31:26 -07:00
JonnyWong16
ad6e314343 Don't floor newsletter start date 2020-09-27 17:44:11 -07:00
JonnyWong16
2a4b48d0fa Clean up Telegram send poster 2020-09-26 19:03:24 -07:00
JonnyWong16
a8e0502b41 Merge pull request #1377 from JohnnyKing94/master
Added Silent Notification option for Telegram Agent
2020-09-26 18:44:58 -07:00
JonnyWong16
ccf0e0dae7 Add default thumb and art to Live TV library 2020-09-26 18:32:13 -07:00
JonnyWong16
bfa4d3dfec Add library name to fix metadata modal 2020-09-26 18:03:29 -07:00
JonnyWong16
93997c11dc Add playback error notification trigger 2020-09-21 18:31:19 -07:00
JonnyWong16
7ce92d5f17 Add error state icon to activity card and history table 2020-09-21 18:30:41 -07:00
JonnyWong16
9740010368 Add container_decision to notification parameters 2020-09-21 18:06:40 -07:00
JonnyWong16
e4e0b765b6 Rename container transcoding to converting on activity cards 2020-09-21 17:57:01 -07:00
Gianfranco
721cf5c930 Renamed 'silent_message' to 'silent_notification.'
Signed-off-by: Gianfranco <gianfry94@hotmail.it>
2020-09-14 20:46:22 +02:00
Gianfranco
f07bcca96a Wording changes
Signed-off-by: Gianfranco <gianfry94@hotmail.it>
2020-09-14 20:26:27 +02:00
JonnyWong16
056d0d81ac Improve activity monitor session start log message 2020-09-14 09:19:33 -07:00
JonnyWong16
38ccd37867 Fix QR code not showing up for localhost address 2020-09-14 08:51:22 -07:00
Gianfranco
21799116c5 Reworked the Telegram Agent code to include the "silent_message" option. Both cases are now managed and the alerts are being respected.
Signed-off-by: Gianfranco <gianfry94@hotmail.it>
2020-09-14 12:46:45 +02:00
JonnyWong16
60bdf1d1ce Schedule database pragma optimize 2020-09-12 14:18:31 -07:00
JonnyWong16
02658759ea Fix purge library from the edit modal 2020-09-10 08:35:26 -07:00
JonnyWong16
68946ceede Add uninstall before installing to Windows installer 2020-09-06 19:03:47 -07:00
JonnyWong16
9184ae4608 v2.5.5 2020-09-06 14:43:16 -07:00
JonnyWong16
de64b5ddfa Revert "Add negative margin to sections with fixed cards"
This reverts commit 668c9e6045.

* Revert #1378
2020-09-06 14:18:25 -07:00
JonnyWong16
b3ffbbf3ea Patch osxnotify for Python 3 compatibility
* Fixes Tautulli/Tautulli-Issues#276
2020-09-06 14:14:08 -07:00
JonnyWong16
aa80fdf738 Merge pull request #1380 from mvanbaak/issue_277_update_ipaddr
Upgrade ipaddr from 2.1.11 to 2.2.0. Its now python 3 compatible

* Fixes Tautulli/Tautulli-Issues#277
2020-09-06 13:59:13 -07:00
JonnyWong16
9ad95f51d4 Fix whois lookup failing in some instances 2020-09-06 13:57:42 -07:00
JonnyWong16
0902a61341 Update profilehooks to 1.12.0
* Fixes Tautulli/Tautulli-Issues#275
2020-09-06 13:45:01 -07:00
JonnyWong16
55ffd54e5b Filter out background theme music sessions 2020-09-04 18:29:37 -07:00
JonnyWong16
e014bfa63e Log selected Plex server 2020-09-04 18:27:28 -07:00
JonnyWong16
687672e9c1 Fix plural seasons in recently added 2020-09-04 08:01:51 -07:00
Michiel van Baak
137889dc9c Upgrade ipaddr from 2.1.11 to 2.2.0. Its now python 3 compatible 2020-09-03 16:46:37 +02:00
JonnyWong16
f24f4a4250 Fix checking pid file on startup 2020-08-29 12:35:04 -07:00
JonnyWong16
95fc108d57 Merge pull request #1378 from dotsam/margin-fix
Add negative margin to sections with fixed cards
2020-08-29 12:31:58 -07:00
JonnyWong16
95f48ba9f6 Return empty results for API instead of null error
* Fixes Tautulli/Tautulli-Issues#274
2020-08-26 18:56:49 -07:00
JonnyWong16
d80cf232c8 Add multi-column sorting to datatables API commands 2020-08-26 18:32:06 -07:00
JonnyWong16
ab3ec875a3 Return custom library art for datatable 2020-08-26 17:53:50 -07:00
Sam Edwards
668c9e6045 Add negative margin to sections with fixed cards 2020-08-23 23:14:45 -07:00
JonnyWong16
67b452a461 Fix user and library recently played sorting order 2020-08-23 18:17:41 -07:00
Gianfranco
81ee44b60f Added "silent_message" option for Telegram Agent
Added a new checkbox in the notification telegram config in order to send new messages silently. In this way the telegram users will receive a notification with no sound.
2020-08-21 22:49:26 +02:00
JonnyWong16
9b3bfd14db Check external guids for notification parameters 2020-08-21 11:58:07 -07:00
JonnyWong16
e00c8fb186 Add external guids to metadata 2020-08-21 11:57:43 -07:00
JonnyWong16
a0919e246d Use pyinstaller==3.6 for package builds 2020-08-21 11:10:14 -07:00
JonnyWong16
003f684f8a Override thumb for clips using the art rating_key 2020-08-21 10:53:05 -07:00
JonnyWong16
69d55c60c3 Add icon and thumb for clips on history table 2020-08-21 10:53:05 -07:00
JonnyWong16
560094dcf6 Add logger message for generating newsletter 2020-08-21 10:53:05 -07:00
JonnyWong16
4edd6ce911 Add scheduled task to optimize database 2020-08-21 10:52:26 -07:00
JonnyWong16
f76bd2af8e Merge pull request #1376 from nwithan8/patch-1
Spelling error
2020-08-11 14:32:21 -07:00
Nate Harris
7747503fee Spelling error 2020-08-09 22:40:47 -04:00
JonnyWong16
1e1a8ddfb0 Fix get_logs API command encoding error
* Fixes Tautulli/Tautulli-Issues#269
2020-08-08 21:37:09 -07:00
JonnyWong16
9bcd18f1b6 Remove revealed characters in masked log 2020-08-08 21:31:50 -07:00
JonnyWong16
50b6f9a8f2 Blacklist password parameter in get_apikey command 2020-08-08 21:26:01 -07:00
JonnyWong16
b4ba88b3e5 Fix get_apikey API command with a hashed password 2020-08-08 21:19:39 -07:00
JonnyWong16
ba9acd6e23 Add auth to some admin endpoints 2020-08-05 21:21:19 -07:00
JonnyWong16
dd9513313b Don't highlight links in scheduler table 2020-08-05 20:56:44 -07:00
JonnyWong16
288a1c86ab Replace white with "not white" 2020-08-05 20:54:12 -07:00
JonnyWong16
8e28cb10fa Rename terminate session to terminate stream 2020-08-05 09:02:18 -07:00
JonnyWong16
3d35a525d3 Make sure json response is encoded to utf-8 2020-08-03 21:45:11 -07:00
JonnyWong16
f7153d0f3b Fix Local user icon not showing in library user stats 2020-08-03 11:45:43 -07:00
JonnyWong16
4285b55c15 Update timestamp helper functions 2020-08-03 10:29:45 -07:00
JonnyWong16
b54576f08f Fix download API commands not returning the file
* Fixes Tautulli/Tautulli-Issues#268
2020-08-02 22:09:40 -07:00
JonnyWong16
6b4db681ff Fix get_synced_items API command returning error with empty result
* Fixes Tautulli/Tautulli-Issues#267
2020-08-02 22:03:35 -07:00
JonnyWong16
f582f781f3 Update helpers.now function 2020-08-02 13:48:10 -07:00
JonnyWong16
9baecb0a41 Change webstart failure error message 2020-08-02 10:29:57 -07:00
JonnyWong16
91a18e1a92 Add get_server_info API command 2020-08-02 10:18:57 -07:00
JonnyWong16
acfbb0e96d Add import_config to API docs 2020-08-02 10:18:25 -07:00
JonnyWong16
c52292962d Remove mock 2020-07-31 22:06:23 -07:00
JonnyWong16
6e53743716 Update plexapi to v3.6.0 2020-07-31 22:06:07 -07:00
JonnyWong16
873194b402 Add hidden import pkg_resources.py2_warn to Windows installer 2020-07-31 15:14:31 -07:00
JonnyWong16
21dec5feb3 v2.5.4 2020-07-31 14:24:08 -07:00
JonnyWong16
bee4106af0 Change direct stream icon 2020-07-31 14:23:01 -07:00
JonnyWong16
bbb6e46515 Replace sys.stderr with logger 2020-07-27 18:47:08 -07:00
JonnyWong16
570ebb4f73 Add plex_id to notification parameters 2020-07-27 18:38:04 -07:00
JonnyWong16
d93204af4e Lookup TVmaze using title 2020-07-27 18:30:00 -07:00
JonnyWong16
702f116db9 Lookup The Movie Database using title and year 2020-07-27 18:20:12 -07:00
JonnyWong16
0c8607b3ec Fix typo in QR modal 2020-07-25 13:28:39 -07:00
JonnyWong16
3a2cc6efc7 Trim address when generating the QR code 2020-07-25 13:28:39 -07:00
JonnyWong16
1b37ff1655 Mobile device registration temporarily assume device_id is onesignal_id 2020-07-25 13:28:34 -07:00
JonnyWong16
769934c8a5 Add server_id to Andoird App notification data 2020-07-25 13:21:52 -07:00
JonnyWong16
7f1a4ec34a Add return PMS name and server ID from device registration 2020-07-25 13:21:52 -07:00
JonnyWong16
27438f7915 Don't allow apikey when using an app 2020-07-25 13:21:51 -07:00
JonnyWong16
8651bef9c1 Mask onesignal_id from API logs 2020-07-25 13:21:51 -07:00
JonnyWong16
36324d10dc Add onesignal_id to register device API 2020-07-25 13:21:46 -07:00
JonnyWong16
0272c35047 Fix parsing request responst message 2020-07-25 11:59:24 -07:00
JonnyWong16
70c0f912e2 Add themoviedb rating image 2020-07-24 09:12:11 -07:00
JonnyWong16
59a6acc088 Fix encoding issue with websocket logging 2020-07-23 17:44:17 -07:00
JonnyWong16
10b0726727 Remove support for .exe from script notifications 2020-07-23 17:33:43 -07:00
JonnyWong16
8f1360d7c2 Check for valid script extension when using a prefix override
* Also removes php, ruby, and perl overrides
2020-07-23 17:33:36 -07:00
JonnyWong16
e0e5ac9ecc Check for a valid script and script extension 2020-07-22 18:55:14 -07:00
JonnyWong16
c814f219a2 Prevent simultaneous importing of database/config 2020-07-22 18:33:47 -07:00
JonnyWong16
9095fc0c7a Catch config.ParseError 2020-07-22 18:27:23 -07:00
JonnyWong16
a675202537 Browse path starting from from current value 2020-07-18 15:19:42 -07:00
JonnyWong16
b52ab4885b Add browser button for script folder 2020-07-18 12:13:42 -07:00
JonnyWong16
43e26c9b56 Add Plex logs folder to config not imported note 2020-07-16 19:38:20 -07:00
JonnyWong16
703a7feed2 Update help text for SSL certificates/key in PEM format 2020-07-16 19:29:11 -07:00
JonnyWong16
7b69ed4cec Add browse function to settings with a folder or file 2020-07-16 19:27:14 -07:00
JonnyWong16
fcca7f969e Add filter extension as data property 2020-07-16 19:26:24 -07:00
JonnyWong16
ec34ea2116 Trigger change and unbind after selecting in file browser 2020-07-16 19:05:18 -07:00
JonnyWong16
3dc36c3b92 Refactor browse path function 2020-07-16 18:19:43 -07:00
JonnyWong16
f0d4fd5523 Add placeholder text for database/config import 2020-07-16 18:19:20 -07:00
JonnyWong16
7fe6c72fe2 Do not import PMS logs folder from config 2020-07-16 18:01:47 -07:00
JonnyWong16
d216d0f27f Reword import help text 2020-07-16 00:02:13 -07:00
JonnyWong16
43a7758acd Cleanup database import modal 2020-07-15 23:53:01 -07:00
JonnyWong16
3043956dec Add config import to settings page 2020-07-15 23:51:48 -07:00
JonnyWong16
06665fdd06 Add fucntion to import a config file 2020-07-15 23:26:22 -07:00
JonnyWong16
beff5caaac Clean shutdown page 2020-07-15 22:53:46 -07:00
JonnyWong16
3859412b2c Fix database import API docs 2020-07-15 22:10:17 -07:00
JonnyWong16
f7ec476fc0 Remove more unused config keys 2020-07-15 21:25:34 -07:00
JonnyWong16
b97d32671d Remove unused library update functions 2020-07-15 21:21:17 -07:00
JonnyWong16
01c56ef280 Remove helper bool check in database import status 2020-07-15 21:06:28 -07:00
JonnyWong16
b9422312f3 Remove unused check recently added pinger 2020-07-15 21:05:49 -07:00
JonnyWong16
9a0f83c3e7 Remove old config updates 2020-07-15 21:04:30 -07:00
JonnyWong16
fbfedb2e62 Remove unused config keys 2020-07-15 21:04:22 -07:00
JonnyWong16
4f8a462041 Update chown instructions in systemd script 2020-07-13 19:08:20 -07:00
JonnyWong16
141d043a6a FreeBSD/FreeNAS python is python3 2020-07-13 19:08:19 -07:00
JonnyWong16
c1266fed12 Update API docs for database import 2020-07-13 19:08:14 -07:00
JonnyWong16
4a4be9798d Adjust user IP table column widths 2020-07-12 12:54:56 -07:00
JonnyWong16
172692ccca Fix user IP table showing first played instead of last played 2020-07-12 12:54:42 -07:00
JonnyWong16
50e7c0469f Merge pull request #1374 from dotsam/ip-first-streamed
Add first_seen to user IP Table
2020-07-12 12:41:46 -07:00
JonnyWong16
44f74e3590 Mask device token and device id from API logs 2020-07-12 10:35:52 -07:00
Sam Edwards
63656b73c2 Add first_seen to user ips and add title attr with full date/time 2020-07-11 15:23:26 -07:00
JonnyWong16
40ecf56904 Fix Cloudinary upload for non-ASCII characters on Python 2 2020-07-10 21:57:53 -07:00
JonnyWong16
b4a10adec2 Merge branch 'v2.5-monitor-remote-access' into nightly 2020-07-10 17:09:58 -07:00
JonnyWong16
1698622d63 v2.5.3 2020-07-10 17:07:18 -07:00
JonnyWong16
fa27271647 Change shebang on contrib scripts 2020-07-10 17:02:23 -07:00
JonnyWong16
d837811c68 Improve start script 2020-07-09 17:13:16 -07:00
JonnyWong16
ad195f0969 Fix deleteing more than 1000 history entries at the same time 2020-07-08 12:27:20 -07:00
JonnyWong16
4a8748e322 Live TV library not being recreated after server identifier is changed
* Fixes Tautulli/Tautulli-Issues#261
2020-07-07 18:14:00 -07:00
JonnyWong16
0f016c83ea Fix ipwhois data location for macOS package
* Fixes Tautulli/Tautulli-Issues#260
2020-07-07 17:25:46 -07:00
JonnyWong16
061ae44da4 Fix indentation in macOS postinstall script 2020-07-07 17:05:15 -07:00
JonnyWong16
a8b90bf100 Reduce macOS build requirement to pyobjc-framework-Cocoa 2020-07-07 17:05:10 -07:00
JonnyWong16
eb3cd49bc4 Add hidden import pkg_resources.py2_warn to macos.spec
* Fixes build on macOS 10.13 (High Sierra)
2020-07-06 20:57:37 -07:00
JonnyWong16
416d869288 Add python version to Google Analytics 2020-07-06 18:13:33 -07:00
JonnyWong16
a116c26c25 Run python scripts with the same sys.executable as Tautulli 2020-07-06 11:32:16 -07:00
JonnyWong16
cc4ec53dac Full path to python3 interpreter in FreeBSD startup script 2020-07-06 10:08:36 -07:00
JonnyWong16
63164c7ff5 Quote command in systemd script 2020-07-06 09:37:37 -07:00
JonnyWong16
9815c014e8 Add python interpreter to init-scripts 2020-07-06 09:30:35 -07:00
JonnyWong16
69675151bf Remove monitor remote access settings
* Tautulli/Tautulli-Issues#251
2020-07-05 20:40:44 -07:00
JonnyWong16
99e395ddfa Update scheduled tasks table
* Tautulli/Tautulli-Issues#251
2020-07-05 20:39:31 -07:00
JonnyWong16
7fe1e542df Remove check remote access scheduled task
* Tautulli/Tautulli-Issues#251
2020-07-05 20:38:54 -07:00
JonnyWong16
938134081b Add remote access monitoring using websockets
* Fixes Tautulli/Tautulli-Issues#251
2020-07-05 20:36:44 -07:00
JonnyWong16
3fd2234a92 Remove refresh reachability 2020-07-05 19:20:52 -07:00
JonnyWong16
41843dc573 Rename some column headers 2020-07-04 12:22:40 -07:00
JonnyWong16
cc6bd528a5 Add architecture to release assets 2020-07-04 11:28:22 -07:00
JonnyWong16
2625ef5fb9 Use Popen to restart on macOS 2020-07-03 19:48:27 -07:00
JonnyWong16
dbd2d28877 Set macOS menu bar icon thread to daemon 2020-07-03 19:47:57 -07:00
JonnyWong16
f70f814c70 Shutdown tray icons last 2020-07-03 19:47:11 -07:00
JonnyWong16
6710e42134 Hide macOS dock icon for pkg install 2020-07-03 19:46:27 -07:00
JonnyWong16
78c5b45e43 Also fix e562ec9 for Python 2 2020-07-03 11:24:47 -07:00
JonnyWong16
e562ec96fa Fix encoding when reading a newsletter file 2020-07-02 20:46:42 -07:00
JonnyWong16
9b5e01c319 Fix logger for email notification exception 2020-07-02 12:45:45 -07:00
JonnyWong16
0097532f4a Fix startup script 2020-07-02 12:33:32 -07:00
JonnyWong16
91935c9018 Add hidden import cheroot.ssl.builtin for pyinstaller 2020-07-02 09:20:58 -07:00
JonnyWong16
83df807f7e Fix typo in eb3db20 2020-07-02 09:15:13 -07:00
JonnyWong16
eb3db20340 Add hidden import chroot.ssl for pyinstaller 2020-07-02 09:11:15 -07:00
JonnyWong16
6dab6194ea Replace which with command -v in startup script 2020-07-01 22:44:05 -07:00
JonnyWong16
356f64cac0 v2.5.2 2020-07-01 19:49:44 -07:00
JonnyWong16
f77f289125 Move GitHub sponsor first 2020-07-01 15:53:08 -07:00
JonnyWong16
280257477a Revert "Change shebang to python3"
This reverts commit cd8a899521.
2020-07-01 14:59:18 -07:00
JonnyWong16
660141cb16 Try various python versions in startup script 2020-07-01 14:43:35 -07:00
JonnyWong16
cd8a899521 Change shebang to python3 2020-07-01 14:43:04 -07:00
JonnyWong16
cb577c51b8 v2.5.2-beta 2020-06-27 15:04:06 -07:00
JonnyWong16
1c395ab10c Patch SameSite support into cookies
* Python 2.7 is missing SameSite cookie attribute
2020-06-27 15:01:16 -07:00
JonnyWong16
07d7170e49 v2.5.1-beta 2020-06-26 18:37:07 -07:00
JonnyWong16
88e23627fd Fix typo 2020-06-25 19:12:10 -07:00
JonnyWong16
48f846da40 Expire the previous JWT on update if HTTP root is set
* Required for Tautulli/Tautulli-Issues#255
2020-06-24 14:04:07 -07:00
JonnyWong16
ff887d9948 Remove unnecessary x_plex_headers from 805d45b 2020-06-23 20:07:45 -07:00
JonnyWong16
617b0d6fd9 Set JWT cookie path to HTTP root
* Fixes Tautulli/Tautulli-Issues#255
2020-06-23 20:00:50 -07:00
JonnyWong16
805d45bd33 Don't overwrite PMS_UUID when fetching a new token 2020-06-23 19:47:01 -07:00
JonnyWong16
fef428202f Start Tautulli using different user in Docker container 2020-06-21 12:38:27 -07:00
JonnyWong16
40fd82febd Only change Docker permissions if PUID/PGID exists 2020-06-21 10:38:26 -07:00
JonnyWong16
45f0001da5 Fix Docker permissions if pre-existing PUID/PGID 2020-06-21 09:58:29 -07:00
JonnyWong16
c7a3e1e3bf Change Docker default PUID and PGID 2020-06-21 00:27:48 -07:00
JonnyWong16
9dd8cc9e49 Fix Docker container not using PUID and PGID environment variables 2020-06-20 23:51:29 -07:00
JonnyWong16
d252d4cd2d Update Publish Docker workflow 2020-06-20 23:51:21 -07:00
JonnyWong16
bc1328040c Update Publish Release workflow 2020-06-20 23:51:20 -07:00
JonnyWong16
82919d3c1d Fix indent in MacOS postinstall script 2020-06-20 23:51:19 -07:00
JonnyWong16
7c801c2f5e Add flag for offical mobile app 2020-06-20 16:16:35 -07:00
JonnyWong16
9a932aea12 Fix text wrapping on user player stats 2020-06-20 15:03:51 -07:00
JonnyWong16
5696e75abe Add LG platform icon 2020-06-20 15:03:32 -07:00
JonnyWong16
efb3f748c2 Improve app registration instructions 2020-06-20 11:36:54 -07:00
JonnyWong16
450b3865a8 Validate OneSignal Player ID when registering device 2020-06-20 10:59:55 -07:00
JonnyWong16
970667adca Only allow temporary device token access to register app 2020-06-20 10:58:49 -07:00
JonnyWong16
89307dad01 Show missing pyobjc module message on MacOS menu bar setting 2020-06-14 15:22:58 -07:00
JonnyWong16
451feda86b Rename system tray to menu bar on MacOS 2020-06-14 14:59:37 -07:00
JonnyWong16
4d241fac48 Try import rumps 2020-06-14 14:52:55 -07:00
JonnyWong16
4390f5cbc8 Check for Foundation module for MacOS system track icon
* Fixes Tautulli/Tautulli-Issues#249
2020-06-13 14:36:47 -07:00
JonnyWong16
7f9d46eac3 Fix Cloudinary upload for Python 2 2020-06-03 20:41:57 -07:00
JonnyWong16
d0f28883aa Remove ability to login using Plex username / password.
* Only login using Plex OAuth
2020-06-02 17:28:24 -07:00
JonnyWong16
48203e64a9 Improve test browser notifications 2020-06-01 22:55:59 -07:00
JonnyWong16
42b17ca495 Change default recently added notification delay to 300s 2020-06-01 16:44:01 -07:00
JonnyWong16
d8080fe506 Fix creating self-signed certificates on Python 3
* Fixes Tautulli/Tautulli-Issues#248
2020-06-01 16:40:25 -07:00
JonnyWong16
be910e24f7 Update release workflow
* Update joncloud/makensis-action@v1.2
2020-05-31 15:35:55 -07:00
JonnyWong16
ce6d70f6fd Fix CHANGELOG.md 2020-05-31 15:29:40 -07:00
JonnyWong16
827e05e4d7 Update release workflow
* Update joncloud/makensis-action@v2
2020-05-31 15:29:23 -07:00
JonnyWong16
43e40e99f1 v2.5.0-beta 2020-05-31 14:51:18 -07:00
JonnyWong16
d95afa990d Auto collapse news items after a week 2020-05-31 14:47:08 -07:00
JonnyWong16
e14457da58 Update README.md 2020-05-31 14:31:01 -07:00
JonnyWong16
9613934ae5 Add symlink for init.freenas -> init.freebsd 2020-05-31 14:17:31 -07:00
JonnyWong16
07a48c04d7 Improve PMS verify error message in setup wizard 2020-05-28 19:22:55 -07:00
JonnyWong16
fbcf59abf0 Add database import in progress message 2020-05-24 01:10:52 -07:00
JonnyWong16
2ef40a6a1c Remove shadow database module name 2020-05-24 01:10:26 -07:00
JonnyWong16
5b5c4d1a8b Ignore reference_id when deleting duplicate rows 2020-05-24 00:36:41 -07:00
JonnyWong16
5f2a74893a Fix importing using the overwrite method 2020-05-24 00:18:10 -07:00
JonnyWong16
0741b4021c Fix database exception 2020-05-24 00:13:29 -07:00
JonnyWong16
f2323b0dff Sort import database tables 2020-05-24 00:13:17 -07:00
JonnyWong16
0462121f69 Fix deleteing duplicate rows from session history tables after import 2020-05-24 00:01:58 -07:00
JonnyWong16
fe4ddaeb52 Show notification sent when testing Browser notification 2020-05-23 17:54:55 -07:00
JonnyWong16
bdbfafabbd Append suffix to uploaded database 2020-05-23 16:27:23 -07:00
JonnyWong16
42c6340c06 Delete uploaded file if invalid database 2020-05-23 16:02:45 -07:00
JonnyWong16
39e1caec0f Add delete file helper function 2020-05-23 16:02:07 -07:00
JonnyWong16
ef72832e5a Redo importing session history 2020-05-23 15:48:40 -07:00
JonnyWong16
39eb657012 Fix typo when importing session_history table names 2020-05-19 22:41:49 -07:00
JonnyWong16
b8f8d45807 Skip importing table if it doesn't exist 2020-05-19 22:40:53 -07:00
JonnyWong16
b01fefc235 Check for existing column names when importing 2020-05-19 21:57:04 -07:00
JonnyWong16
09f6eb8e19 Fix importing into an empty database 2020-05-19 21:31:22 -07:00
JonnyWong16
e5d4969917 Fix imports for deleting history on Python 2 2020-05-19 20:18:38 -07:00
JonnyWong16
53aa740305 Supress InsecureRequestWarning for requests without ssl verify 2020-05-16 17:30:44 -07:00
JonnyWong16
9a00350ffc Add option to disable websocket SSL cert verify 2020-05-16 17:30:03 -07:00
JonnyWong16
98ffa3735b Add verify ssl certificate to websocket connection 2020-05-16 17:16:53 -07:00
JonnyWong16
9073568c0f Set branch to nightly 2020-05-16 16:26:55 -07:00
JonnyWong16
17a01d65aa Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/version.py
#	plexpy/webserve.py
2020-05-16 16:22:10 -07:00
JonnyWong16
5089575aac Remove python interpeter from systemd script (Closes #1373) 2020-05-16 16:19:34 -07:00
JonnyWong16
7f178e0913 v2.2.4 2020-05-16 16:13:03 -07:00
JonnyWong16
dcad3017d3 Fix get_history returning incorrect title (Fixes Tautulli/Tautulli-Issues#244) 2020-05-16 16:11:25 -07:00
JonnyWong16
ae88489e55 Remove extra spaces in notifier config text accordion 2020-05-10 14:18:54 -07:00
JonnyWong16
b57065d6ee Add Tautulli news to settings page 2020-05-10 14:18:36 -07:00
JonnyWong16
71551d3f6d Show "None" for source subtitle if user selected subtitle 2020-05-09 17:25:46 -07:00
JonnyWong16
cbcad30a6c Fix guest login filters 2020-05-09 16:19:30 -07:00
JonnyWong16
e2c2f66e97 Update Plex.tv signin to /api/v2 2020-05-09 16:19:12 -07:00
JonnyWong16
eeff665680 Fix form login using Plex.tv credentials 2020-05-09 15:36:20 -07:00
JonnyWong16
6ef9d187ba Fix remote access check rescheduled when settings are saved 2020-05-09 15:16:07 -07:00
JonnyWong16
6d23ef9105 Decode websocket data 2020-05-09 15:06:26 -07:00
JonnyWong16
6c8b425fb3 Improve browsing for path on Windows 2020-05-08 17:54:14 -07:00
JonnyWong16
d4b46a5721 Get stream user by user_id instead of username 2020-05-07 12:25:23 -07:00
JonnyWong16
9d2be4b939 Fix typo 2020-05-04 14:29:46 -07:00
JonnyWong16
bc017fb010 Fix Plex Android/iOS notification agent settings not opening 2020-05-04 13:43:52 -07:00
JonnyWong16
bfabbe3cdb Fix library stuck as inactive in the database 2020-05-04 13:03:49 -07:00
JonnyWong16
8a8d47f8e7 Add try again message to database import 2020-05-03 18:39:00 -07:00
JonnyWong16
b01fac9641 Fix loging on Python 2 2020-05-03 18:34:29 -07:00
JonnyWong16
25c850e243 Increase file upload size to 1GB 2020-05-03 18:11:49 -07:00
JonnyWong16
8c7476a670 Only use form data if uploading a database file 2020-05-03 17:49:10 -07:00
JonnyWong16
12effd643f Sort folders and files in file browser 2020-05-03 17:13:17 -07:00
JonnyWong16
209008e50d Decode browse path 2020-05-03 17:03:22 -07:00
JonnyWong16
b336f07ff9 Improve database import error messages 2020-05-03 16:52:17 -07:00
JonnyWong16
73f6012507 Clear database file name after uploading 2020-05-03 16:12:26 -07:00
JonnyWong16
b73564d2e0 Add Tautulli database to welcome wizard import message 2020-05-03 15:19:36 -07:00
JonnyWong16
00adb45086 Add launch browser toggle to system tray 2020-05-03 15:16:38 -07:00
JonnyWong16
d604d40e91 Missing comma 2020-05-03 14:53:38 -07:00
JonnyWong16
ba3f6935db Fix missing ipwhois data in bundle package 2020-05-03 14:52:29 -07:00
JonnyWong16
980c4f7618 Add option to upload or browse for a database file to import 2020-05-03 14:33:25 -07:00
JonnyWong16
a869859491 Improve validating database before import 2020-05-02 23:14:17 -07:00
JonnyWong16
15a638b86e Keep primary key instead of re-indexing history when overwriting Tautulli database import 2020-05-02 22:06:04 -07:00
JonnyWong16
e999000102 Fix overwrite Tautulli database importing 2020-05-02 21:56:39 -07:00
JonnyWong16
95bdc000ca Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/version.py
2020-05-02 14:05:41 -07:00
JonnyWong16
1d46efe037 Add bundle to status table in readme 2020-05-02 13:00:03 -07:00
JonnyWong16
5499e89058 Don't show deleted library on homepage cards 2020-05-02 12:36:04 -07:00
JonnyWong16
a5653e365e Improve merge database import method and remove append method 2020-05-01 19:32:19 -07:00
JonnyWong16
35a0242037 v2.2.3 2020-05-01 09:22:35 -07:00
JonnyWong16
e698bcb375 Skip importing temporary sessions table 2020-04-30 22:16:40 -07:00
JonnyWong16
33d5aca6d4 Add note that settings also imported with database 2020-04-30 22:15:44 -07:00
JonnyWong16
058bd32329 Update import_database API docs 2020-04-30 22:11:06 -07:00
JonnyWong16
52d38883dc Add Tautulli database import to the settings page 2020-04-30 22:06:54 -07:00
JonnyWong16
c1d98ab901 Add method to import a Tautulli database 2020-04-30 22:06:41 -07:00
JonnyWong16
e555b7e456 Add index to sessions_continued database table 2020-04-30 17:55:50 -07:00
JonnyWong16
031bef8c02 Add index to image lookup database tables 2020-04-30 17:44:27 -07:00
JonnyWong16
3bf138e2ad Add branch build installer workflow 2020-04-30 17:37:06 -07:00
JonnyWong16
4e0563bbf9 Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/activity_pinger.py
#	plexpy/webserve.py
2020-04-28 18:44:41 -07:00
JonnyWong16
e2e7063a29 Expose remote access check settings in the UI 2020-04-28 18:39:12 -07:00
JonnyWong16
03035d0eac Prevent spamming the logs with remote access failures 2020-04-28 18:04:05 -07:00
JonnyWong16
7ce9283421 Fix geopip lookup for IPv6 addresses 2020-04-28 08:42:00 -07:00
JonnyWong16
a8783ac351 Check if Windows registry value exists before trying to delete 2020-04-27 19:49:14 -07:00
JonnyWong16
fb51894fad Update build installer workflow 2020-04-27 19:35:13 -07:00
JonnyWong16
25d65e8d65 Add temporary test builds 2020-04-27 19:30:32 -07:00
JonnyWong16
3e10e0e511 Python 2 compatible Windows imports 2020-04-27 19:03:45 -07:00
JonnyWong16
c3245c1f03 Remove keep alive from MacOS plist 2020-04-27 18:24:46 -07:00
JonnyWong16
5b022599b4 Add build requirements.txt 2020-04-27 18:24:45 -07:00
JonnyWong16
d5d219d46f Do not auto-shutdown Tautulli in installers 2020-04-27 18:24:45 -07:00
JonnyWong16
e546689e01 Check if MacOS login item exists before adding 2020-04-27 18:24:44 -07:00
JonnyWong16
cac9e0b164 Add startup scripts 2020-04-27 18:24:44 -07:00
JonnyWong16
4bb5920c04 Fix only MacOS system tray icon on Mac 2020-04-27 18:24:44 -07:00
JonnyWong16
3ea257f8f3 Add MacOS pkg post install script to open Tautulli 2020-04-27 18:24:43 -07:00
JonnyWong16
347db6b770 Update workflow for beta releases 2020-04-27 18:24:43 -07:00
JonnyWong16
fafe28a6d6 Shutdown MacOS system tray icon 2020-04-27 18:24:42 -07:00
JonnyWong16
eb6cb60ee3 Fix toggle startup in MacOS system tray menu 2020-04-27 18:24:42 -07:00
JonnyWong16
8226a14b00 Add dependencies for MacOS system tray icon 2020-04-27 18:24:41 -07:00
JonnyWong16
c6bd1b06f2 Hide auto update setting for bundled app 2020-04-27 18:24:37 -07:00
JonnyWong16
be38028244 Fix MacOS tray icon 2020-04-27 18:23:38 -07:00
JonnyWong16
b8ea04f5a4 Add divder to MacOS system tray menu 2020-04-27 18:23:37 -07:00
JonnyWong16
cd5ed1d748 Update system tray icon 2020-04-27 18:23:37 -07:00
JonnyWong16
00c9fc79f9 Rename sys_tray_icon setting 2020-04-27 18:23:34 -07:00
JonnyWong16
d5373c3992 Add MacOS system tray icon 2020-04-27 18:22:50 -07:00
JonnyWong16
3001ff8c53 Clean up Windows tray icon 2020-04-27 18:22:49 -07:00
JonnyWong16
0571a091f7 Add rumps 0.3.0 2020-04-27 18:22:49 -07:00
JonnyWong16
1bca410bcb Launch browser on system startup based on setting 2020-04-27 18:22:48 -07:00
JonnyWong16
463ed2f46a Add launch startup to setup wizard to make sure it's enabled 2020-04-27 18:22:48 -07:00
JonnyWong16
f8f0717913 Refactor Windows system tray code 2020-04-27 18:22:47 -07:00
JonnyWong16
53cd759422 Fix MacOS login items application path 2020-04-27 18:22:47 -07:00
JonnyWong16
7047ac8007 Add to MacOS login item when installed as pkg 2020-04-27 18:22:47 -07:00
JonnyWong16
2efd81dc6a Add more logging 2020-04-27 18:22:46 -07:00
JonnyWong16
773ee8664c Fix create MacOS plist file 2020-04-27 18:22:46 -07:00
JonnyWong16
d779e72bcd Add system launch setting for MacOS 2020-04-27 18:22:45 -07:00
JonnyWong16
2e101dcf7d Add MacOS set startup plist file 2020-04-27 18:22:44 -07:00
JonnyWong16
e6befab6bb Change os.name to common.PLATFORM 2020-04-27 18:22:44 -07:00
JonnyWong16
9ee2c1f7a6 Add log message for failed Windows startup registry 2020-04-27 18:22:43 -07:00
JonnyWong16
5b82a86fa8 Always no browser at Windows system startup 2020-04-27 18:22:43 -07:00
JonnyWong16
922bb2760c Update systray lib 2020-04-27 18:22:42 -07:00
JonnyWong16
315be9f3eb Update Windows system tray with start at login option 2020-04-27 18:22:42 -07:00
JonnyWong16
7bb9c6c915 Set Windows launch at startup 2020-04-27 18:22:41 -07:00
JonnyWong16
4b5f880ccb Fix update message on startup for Windows/MacOS 2020-04-27 18:22:40 -07:00
JonnyWong16
5db309d142 Do not inject libs into PYTHONPATH when installed using Windows/MacOS installer 2020-04-27 18:22:40 -07:00
JonnyWong16
5d8a7d80eb Require manual download and install for Windows / MacOS 2020-04-27 18:22:39 -07:00
JonnyWong16
1394339df6 Use appdata folder 2020-04-27 18:22:39 -07:00
JonnyWong16
801510c61e Add appdirs 1.4.3 2020-04-27 18:22:38 -07:00
JonnyWong16
6c8d6ed2ca Add workflow for automated Windows/MacOS builds 2020-04-27 18:22:34 -07:00
JonnyWong16
d8f223327e Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/activity_pinger.py
#	plexpy/activity_processor.py
#	plexpy/helpers.py
#	plexpy/notifiers.py
#	plexpy/version.py
#	plexpy/webserve.py
2020-04-27 18:19:48 -07:00
JonnyWong16
fc2faa247a v2.2.3-beta 2020-04-27 18:04:27 -07:00
JonnyWong16
9b11fd4f18 Remove all MaxMind/GeoLite2 settings 2020-04-27 17:43:00 -07:00
JonnyWong16
ccac7d1bd4 Remove maxminddb library 2020-04-27 17:34:51 -07:00
JonnyWong16
5494d1e7bf Remove geoip2 library 2020-04-27 17:33:05 -07:00
JonnyWong16
1ab407eb38 Update API to keep backwards compatibility for geoip lookup 2020-04-27 17:29:48 -07:00
JonnyWong16
82ab732144 Use Plex.tv for geoip lookup instead of MaxMind database 2020-04-27 17:28:44 -07:00
JonnyWong16
0343d47a9d Fix request return server message 2020-04-25 13:41:19 -07:00
JonnyWong16
2162210393 Add remote access notification parameters 2020-04-24 18:03:27 -07:00
JonnyWong16
54a7839421 Add remote access failure reason 2020-04-24 18:03:05 -07:00
JonnyWong16
576ac88a6a Add advanced remote access ping interval 2020-04-24 18:01:27 -07:00
JonnyWong16
426fc09b17 Check continued session greater than or equal to 2020-04-23 23:18:55 -07:00
JonnyWong16
22bc0b3f9a Rename continued session to initial stream 2020-04-23 23:03:20 -07:00
JonnyWong16
4ece976dc8 Add continued session threshold setting and notification parameter 2020-04-23 22:47:19 -07:00
JonnyWong16
3ff0b4a256 Add method to check if a stream is a continued session 2020-04-23 22:34:30 -07:00
JonnyWong16
ecfc3ed74f Use server_id when retrieving library details 2020-04-23 22:30:32 -07:00
JonnyWong16
976154ed6c Add episode count to season and year to album Plex Mobile App notifications 2020-04-23 19:05:13 -07:00
JonnyWong16
e527a88a2e Fix Python 2 compatibility import 2020-04-20 21:00:34 -07:00
JonnyWong16
d6b619934a Fix Deprecation Warning for logger.warn 2020-04-20 20:55:54 -07:00
JonnyWong16
c108765857 Add id parameter to get_history API for backwards compatibility 2020-04-19 23:01:58 -07:00
JonnyWong16
96438e1e15 Add id parameter to get_stream_data API for backwards compatibility
* Fixes Tautulli/Tautulli-Issues#239
2020-04-19 14:06:05 -07:00
JonnyWong16
0afd77fb2f Test all Plex mobile app triggers 2020-04-18 20:56:06 -07:00
JonnyWong16
a6cd512ebf Rename Plex Mobile App to Plex Android / iOS App 2020-04-18 20:27:23 -07:00
JonnyWong16
fb5d97a627 Refactor some notifiers code 2020-04-18 19:59:30 -07:00
JonnyWong16
231d439ef8 Remove plex_logs volume from Dockerfile 2020-04-18 19:24:52 -07:00
JonnyWong16
28e48e6b2f Fix MusicBrainzlookup missing artist 2020-04-18 19:24:20 -07:00
JonnyWong16
89c1ec8d21 Fix history table refreshing after deleting 2020-04-18 17:11:44 -07:00
JonnyWong16
3270a60bd7 Add Plex Mobile App notification agent 2020-04-18 17:06:23 -07:00
JonnyWong16
6ccf801ee6 Add code to filter available triggers for notification agents 2020-04-18 15:45:44 -07:00
JonnyWong16
79cd2ca9b9 Add user_thumb to notification parameters 2020-04-18 14:30:03 -07:00
JonnyWong16
063271aabb Fix notification rating key being overwritten when retrieving lookup info 2020-04-18 14:29:49 -07:00
JonnyWong16
e6c2133bf5 Fix auto-updater not working after enabling unless Tautulli was restarted 2020-04-17 18:23:47 -07:00
JonnyWong16
63e056987a Add bandwidth notification parameters 2020-04-17 18:19:27 -07:00
JonnyWong16
93f070f0ac Update Publish Docker workflow 2020-04-15 09:48:49 -07:00
JonnyWong16
df35689c35 Fix typo in CHANGELOG 2020-04-13 11:43:23 -07:00
JonnyWong16
b66e845c6e Fix typo in README 2020-04-13 11:43:04 -07:00
JonnyWong16
3ca4351aeb Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/database.py
#	plexpy/version.py
2020-04-12 21:35:29 -07:00
JonnyWong16
6d5c320701 v2.2.2-beta 2020-04-12 21:27:01 -07:00
JonnyWong16
400a189455 Invalidate temporary mobile app token after 5 minutes 2020-04-12 21:20:14 -07:00
JonnyWong16
b7d03a4f31 Fix refreshing libraries and users table after deleting 2020-04-12 20:51:22 -07:00
JonnyWong16
523e6421be Don't delete library history if server_id doesn't match 2020-04-12 20:49:45 -07:00
JonnyWong16
0ed4b69b8f Fix deleting database rows with Python3 list(map()) 2020-04-12 19:17:57 -07:00
JonnyWong16
94f929743c Merge branch 'nightly' into python3
# Conflicts:
#	.github/workflows/publishdocker-branch.yml
#	Dockerfile
2020-04-12 18:30:34 -07:00
JonnyWong16
e0cd6f7071 Rename docker build arg VERSION to TAG 2020-04-12 18:18:08 -07:00
JonnyWong16
38db0b7a70 Rename VERSION to COMMIT in Dockerfile 2020-04-12 18:15:14 -07:00
JonnyWong16
f39ecd89a7 Docker docker build badges on README 2020-04-12 18:03:40 -07:00
JonnyWong16
f7f76d82b6 Add Docker buildx GitHub workflow 2020-04-12 17:56:15 -07:00
JonnyWong16
9097e79e4f Change web app manifest start url to relative path 2020-04-12 11:33:23 -07:00
JonnyWong16
88711e7601 Add more info to manifest.json 2020-04-12 10:43:30 -07:00
JonnyWong16
d0fa83bb8c Use Kodi platform image for xbmc (Fixes Tautulli/Tautulli-Issues##231) 2020-04-11 20:39:39 -07:00
JonnyWong16
1271458f83 Fix web app manifest file (Fixes Tautulli/Tautulli-Issues#232) 2020-04-11 12:40:51 -07:00
JonnyWong16
2ae09a07e6 Some css syntax fixes 2020-04-11 12:39:12 -07:00
JonnyWong16
9e9ad72dc2 Remove past imports 2020-04-10 15:52:55 -07:00
JonnyWong16
422a89c26c Fix circular helpers import 2020-04-10 15:34:23 -07:00
JonnyWong16
798c17706c Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/database.py
#	plexpy/datafactory.py
#	plexpy/libraries.py
#	plexpy/users.py
2020-04-10 15:25:18 -07:00
JonnyWong16
33d860384c Add Tautulli database corruption notification trigger 2020-04-10 15:11:32 -07:00
JonnyWong16
a4eda99a4a Update API docs to mention enabling api_sql while Tautulli is shut down 2020-04-10 14:44:14 -07:00
JonnyWong16
752c7badd2 Make inactive icon larger on library/user page 2020-04-10 14:41:14 -07:00
JonnyWong16
6399c90642 Fix platform icon size on activity card 2020-04-10 14:36:51 -07:00
JonnyWong16
97089846e9 Make inactive user/library triangle always orange 2020-04-10 14:31:12 -07:00
JonnyWong16
4de7884e39 Update API docs with all delete function changes 2020-04-10 14:14:34 -07:00
JonnyWong16
440adfb914 Fix missing page functions in library table 2020-04-10 14:12:38 -07:00
JonnyWong16
5f26d0085d Simplify library undelete function 2020-04-10 14:07:15 -07:00
JonnyWong16
f484604c69 Simplify user undelete function 2020-04-10 14:07:03 -07:00
JonnyWong16
899d2fbf9d Make library delete server_id optional 2020-04-10 14:03:32 -07:00
JonnyWong16
6a87dc9c40 Improve library delete/purge function 2020-04-10 14:00:19 -07:00
JonnyWong16
104e2929df Simplify user delete loop 2020-04-10 13:27:26 -07:00
JonnyWong16
faac6b11c2 Improve user delete/purge function 2020-04-10 13:15:29 -07:00
JonnyWong16
377a23478e Rename history id to row_id 2020-04-10 12:56:50 -07:00
JonnyWong16
c979e78802 Refactor database delete_session_history_rows ids 2020-04-10 12:40:35 -07:00
JonnyWong16
38f64c7d85 Improve delete history using list of row ids 2020-04-10 11:45:20 -07:00
JonnyWong16
1091a64863 Update API docs with library is_active 2020-04-10 00:20:25 -07:00
JonnyWong16
23de9616f1 Show library active status on Libraries table 2020-04-10 00:17:52 -07:00
JonnyWong16
198e7767dc Add library is_active to database 2020-04-10 00:12:38 -07:00
JonnyWong16
aa31bf1a19 Update API docs with user is_active 2020-04-10 00:11:43 -07:00
JonnyWong16
f366304c50 Show user active status on Users table 2020-04-10 00:02:30 -07:00
JonnyWong16
ce289995ff Add user is_active to database 2020-04-09 23:15:08 -07:00
JonnyWong16
ca2b4085c9 Fix if bad query_days passed to API 2020-04-09 18:33:02 -07:00
JonnyWong16
1d08069162 Rename time_queries to query_days 2020-04-09 18:30:46 -07:00
JonnyWong16
bcbfaae630 Merge pull request #1372 from KaasKop97/custom_time_queries
Allow custom time_queries for get_watch_stats (Closes #1345)
2020-04-09 18:21:35 -07:00
JonnyWong16
0886d133a8 Divide file size by 2^10 but display SI units
(cherry picked from commit ae9df92d28)
2020-04-08 22:56:16 -07:00
JonnyWong16
ae9df92d28 Divide file size by 2^10 but display SI units 2020-04-08 22:55:56 -07:00
JonnyWong16
435230711e Fix middle dot encoding for Discord/Slack notification 2020-04-08 22:46:21 -07:00
JonnyWong16
d75744bb4a Merge branch 'nightly' into python3 2020-04-07 18:40:01 -07:00
JonnyWong16
47610323b0 Fix API grouping parameter not defaulting to match setting 2020-04-07 18:18:16 -07:00
Mitch
d1f1763919 Allow custom time_queries for get_watch_stats 2020-04-05 20:03:57 +02:00
JonnyWong16
1326ad8708 Add TAUTULLI_PYTHON_VERSION to script environment variables
* Period separated string (e.g. 2.7.17 or 3.8.2)
2020-04-04 08:12:42 -07:00
JonnyWong16
86d737dcf6 Add TAUTULLI_PYTHON_VERSION to script environment variables
* Period separated string (e.g. 2.7.17 or 3.8.2)
2020-04-04 08:06:13 -07:00
JonnyWong16
9e0153e962 Set PYTHON2 global variable 2020-04-04 07:57:51 -07:00
JonnyWong16
fb395fc2e9 Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/datafactory.py
2020-04-03 21:10:32 -07:00
JonnyWong16
6e09e509bd Remove duplicate dictionary key in top movie stats 2020-04-03 21:07:00 -07:00
JonnyWong16
e8d0557852 Fix typo in send newsletter argument 2020-04-03 21:06:10 -07:00
JonnyWong16
aac705f465 Put import OpenSSL in try/except block for self-signed certificates 2020-04-03 21:05:44 -07:00
JonnyWong16
009971901b Fix delete lookup info by rating key 2020-04-03 20:53:35 -07:00
JonnyWong16
573ff3f2a6 Fix scripts to work with both Python 2 and 3 2020-04-02 00:03:41 -07:00
JonnyWong16
b9f614c66f Downgrade mock to 3.0.5 for Python 2.7 compatibility
* Only required for plexapi tests
2020-04-01 23:59:20 -07:00
JonnyWong16
e26182c96e Remove list(dict.keys()) --> dict.keys() and list(dict.values()) --> dict.values() 2020-04-01 15:31:15 -07:00
JonnyWong16
f4eff8a8c5 Encode API XML output to UTF-8
(cherry picked from commit 1ffd6c0ea1)
2020-03-30 13:58:08 -07:00
JonnyWong16
1ffd6c0ea1 Encode API XML output to UTF-8 2020-03-30 13:55:17 -07:00
JonnyWong16
b3f8341e0c Fix enable notification grouping by default again
(cherry picked from commit 50ce29cc64)
2020-03-29 21:14:47 -07:00
JonnyWong16
50ce29cc64 Fix enable notification grouping by default again 2020-03-29 21:14:17 -07:00
JonnyWong16
47db4e0559 Fix missing helpers import 2020-03-29 20:57:04 -07:00
JonnyWong16
b8179678c6 Fix Windows system tray icon shortcuts not working 2020-03-29 20:54:20 -07:00
JonnyWong16
c1a7b3753c Merge branch 'nightly' into python3 2020-03-29 10:30:15 -07:00
JonnyWong16
e4ec24be26 Reorder git pull command for update 2020-03-29 10:28:42 -07:00
JonnyWong16
04765288d7 Change default file size on media info tables to SI units 2020-03-29 10:27:56 -07:00
JonnyWong16
8fdd0ba0d9 Merge pull request #1363 from aaronldunlap/master
Change humanFileSize to default to SI notation
2020-03-29 10:18:14 -07:00
JonnyWong16
8b312c8d2d Merge pull request #1369 from Arcanemagus/update-init
Make init scripts Python version agnostic
2020-03-28 17:10:40 -07:00
Landon Abney
ab36041fef Add python to systemd script
Skip the extra process calls trying to figure out what to run Tautulli 
with, as well as give an example on how to change the executable in the 
init script.
2020-03-28 17:01:23 -07:00
Landon Abney
3f87996bfc Remove outdated init scripts
Remove several init scripts for operating systems that are no longer 
supported:
* `init.ubuntu` would only be useful on Ubuntu 14.04 LTS which has been 
in ESM for over a year
* `init.solaris` is for an operating system that hasn't been updated in 
>10 years
* `init.upstart` is for a startup method Ubuntu attempted but abandoned
* `init.fedora.centos.service` is for a version that hasn't recieved 
updates since 2017-05-10
* `init.freenas` is identical to `init.freebsd`
* `init-alt.freebsd` appears to attempt to use the web interface 
directly, and would break with authentication enabled
2020-03-28 16:58:06 -07:00
Landon Abney
4edd2001b3 Make init scripts Python version agnostic
Now that the Tautulli will run on both major versions of Python we can 
remove the specificity in the init scripts and make them simpler, with 
the added advantage that some OS's will now run Tautulli through Python 
3 instead of Python 2.
2020-03-28 16:31:14 -07:00
JonnyWong16
155b98bb0c Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/database.py
#	plexpy/version.py
2020-03-28 15:27:13 -07:00
JonnyWong16
16ffbd9940 v2.2.1 2020-03-28 15:11:02 -07:00
JonnyWong16
f72d93216c Add helper function for timestamp 2020-03-28 14:50:45 -07:00
JonnyWong16
c6cf293b12 Fix duration check for track skipping 2020-03-28 14:49:17 -07:00
JonnyWong16
0f13329ddd Fix saving mobile device with blank friendly name
(cherry picked from commit fa61302954)
2020-03-26 10:12:04 -07:00
JonnyWong16
fa61302954 Fix saving mobile device with blank friendly name 2020-03-26 10:11:33 -07:00
JonnyWong16
1dfbef89ff Add mock 4.0.2 2020-03-24 22:11:26 -07:00
JonnyWong16
c55c00a19e Update PlexAPI to 3.3.0 2020-03-24 22:10:43 -07:00
JonnyWong16
2fa62f71e1 Replace file() with open() 2020-03-24 21:57:52 -07:00
JonnyWong16
846a8cac98 Update Python versions badge on README.md 2020-03-24 21:47:59 -07:00
JonnyWong16
9ee7918e59 Fix Windows system tray icon not enabled by default
(cherry picked from commit 763e5f583a)
2020-03-24 21:25:09 -07:00
JonnyWong16
763e5f583a Fix Windows system tray icon not enabled by default 2020-03-24 21:24:31 -07:00
JonnyWong16
faf5cb0f8d Change jwt_cookie to str 2020-03-24 21:16:14 -07:00
JonnyWong16
bde6309277 Move BaseRedirect to webserve 2020-03-24 21:06:09 -07:00
JonnyWong16
cc05552685 Remove from __future__ import absolute_import 2020-03-24 20:17:44 -07:00
JonnyWong16
465f50666f Fix Tautulli.py import future after lib folder inserted 2020-03-23 23:39:49 -07:00
JonnyWong16
e6d0212604 Remove Python 3 testing from config 2020-03-23 23:32:46 -07:00
JonnyWong16
2eebacc3a6 Improved Mako template exceptions 2020-03-23 23:31:11 -07:00
JonnyWong16
f362880eb6 Fix import for newsletters_table.html 2020-03-23 23:02:28 -07:00
JonnyWong16
68a06d1bbc Remove feedparser 2020-03-23 22:18:54 -07:00
JonnyWong16
82c09570c4 Update all future imports for Python 2 2020-03-23 22:11:42 -07:00
JonnyWong16
58eb426eea Fix UniversalAnalytics import from future 2020-03-23 22:11:42 -07:00
JonnyWong16
1c932057b8 Fix BeautifulSoup imports from future 2020-03-23 22:11:42 -07:00
JonnyWong16
4564623884 Update mako to 1.1.2 2020-03-23 22:11:42 -07:00
JonnyWong16
843a400b2d Fix CustomFormatter for Python 2 2020-03-23 22:11:41 -07:00
JonnyWong16
5b067bd17d Fix opening log file for Python 2 2020-03-23 22:11:41 -07:00
JonnyWong16
ed07bd374c Fix http_handler for Python 2 2020-03-23 22:11:41 -07:00
JonnyWong16
078685a2a3 Fix imports for Python 2 2020-03-23 22:11:41 -07:00
JonnyWong16
2ce5194156 Remove future from Dockerfile 2020-03-23 18:46:54 -07:00
JonnyWong16
fa97d3f88d Add future 0.18.2 2020-03-23 18:45:35 -07:00
JonnyWong16
08c8ee0774 Add ability to flush recently_added database table 2020-03-23 17:50:54 -07:00
JonnyWong16
395fc49087 Add ability to flush recently_added database table 2020-03-23 17:49:51 -07:00
JonnyWong16
9725c82187 Change cron day_of_week for apscheduler 2020-03-23 17:23:42 -07:00
JonnyWong16
24277f1e3c Add favicon to newsletter template
(cherry picked from commit d54794e85f)
2020-03-23 15:22:09 -07:00
JonnyWong16
b58fb1da33 Fix saving newsletter HTML file 2020-03-23 15:21:58 -07:00
JonnyWong16
d54794e85f Add favicon to newsletter template 2020-03-23 15:21:05 -07:00
JonnyWong16
bed1cd8fb5 Fix notification grouping not enabled by default on new install
(cherry picked from commit d5917f89f0)
2020-03-23 10:31:30 -07:00
JonnyWong16
d5917f89f0 Fix notification grouping not enabled by default on new install 2020-03-23 10:28:01 -07:00
JonnyWong16
c2d17c285a Add simplejson 3.17.0
* Needed for requests to encode byte-strings to json
2020-03-21 20:07:36 -07:00
JonnyWong16
42262b0bb6 Android App encrypt requires bytes 2020-03-21 20:05:59 -07:00
JonnyWong16
510dddf724 Remove unused uuid import 2020-03-21 19:46:41 -07:00
JonnyWong16
702b2fe167 Remove Hipchat 2020-03-21 19:22:41 -07:00
JonnyWong16
f24c2a8b77 Don't decode PrettyMetadata episode title dot 2020-03-21 19:13:08 -07:00
JonnyWong16
a675c2c4f2 Fix Cloudinary image upload 2020-03-21 19:12:06 -07:00
JonnyWong16
2984629b39 Update cloudinary to 1.20.0 2020-03-21 19:11:41 -07:00
JonnyWong16
1003aa2df5 Fix related children count 2020-03-21 18:34:04 -07:00
JonnyWong16
1c56d9c513 Fix notification CustomFormatter for Python 3 2020-03-21 18:32:57 -07:00
JonnyWong16
e06210f21c Change notification text format logger to exception 2020-03-21 18:32:38 -07:00
JonnyWong16
ad112e0a44 Remove list(dict.items()) -- >dict.items() 2020-03-21 18:31:55 -07:00
JonnyWong16
2b0e7daf7c Fix error loading removed notification agents configs 2020-03-21 16:28:17 -07:00
JonnyWong16
060dff0162 Update websocket-client to 0.57.0 2020-03-21 12:17:50 -07:00
JonnyWong16
4ae09774f7 Change websocket error to exception to log traceback 2020-03-19 22:54:18 -07:00
JonnyWong16
033a364699 Fix buffer trigger crashing websocket 2020-03-19 21:57:20 -07:00
JonnyWong16
56a66976e6 Fix creating self-signed certificates
* Python 3 does not support tuple unpacking in arguments
2020-03-19 20:57:12 -07:00
JonnyWong16
0f02fab259 Update tzlocal to 2.1b1 2020-03-19 20:38:23 -07:00
JonnyWong16
2917b609c3 Update tzlocal to 2.0.0 2020-03-19 20:30:44 -07:00
JonnyWong16
b9a80d06e4 Automatic python3 Docker workflow 2020-03-19 20:18:57 -07:00
JonnyWong16
af46a02146 python3 branch 2020-03-19 19:56:11 -07:00
JonnyWong16
19d8c1be5a Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/datafactory.py
#	plexpy/libraries.py
#	plexpy/logger.py
#	plexpy/version.py
2020-03-19 19:40:05 -07:00
JonnyWong16
6205af1a9a Fix missing username on sync table 2020-03-17 09:44:10 -07:00
JonnyWong16
d8b1db536c Fix file size notification parameter truncated to integer (Fixes Tautulli/Tautlli-Issues#226) 2020-03-17 09:37:10 -07:00
JonnyWong16
699357ca21 Add transcode decision counts to notification parameters 2020-03-14 15:39:22 -07:00
JonnyWong16
50398049f5 Don't strip tags from webhook agent 2020-03-14 15:31:18 -07:00
JonnyWong16
1f83afc2f4 Update API docs for delete_lookup_info 2020-03-14 14:00:30 -07:00
JonnyWong16
90374bb46f Add buttons to delete all 3rd party metadata lookup info in the settings 2020-03-14 14:00:17 -07:00
JonnyWong16
ccdd410eda Change breadcrumbs on update metadata page to match info page 2020-03-14 13:58:01 -07:00
JonnyWong16
77bb806a01 Add IMDb and Rotten Tomatos rating to info page 2020-03-14 12:07:21 -07:00
JonnyWong16
a952352e1f Get metadata for the info page from the Plex server before database 2020-03-14 12:05:47 -07:00
JonnyWong16
b733ce969a Fix update changelog from beta 2020-03-08 17:03:21 -07:00
JonnyWong16
f4351df302 Combine release workflows 2020-03-08 16:37:19 -07:00
JonnyWong16
76893100fc v2.2.0 2020-03-08 15:37:20 -07:00
JonnyWong16
96e8b808da Fix wording on reset git install note 2020-03-08 15:32:54 -07:00
JonnyWong16
595bff94b4 Add release to reset git install log message 2020-03-08 13:36:29 -07:00
JonnyWong16
5661c00497 Capitalize previous Windows platform in database 2020-03-08 11:26:13 -07:00
JonnyWong16
a98d7bd4eb Remove blank line 2020-03-06 15:43:18 -08:00
JonnyWong16
097203162d Update reset message to show release version 2020-03-06 15:39:15 -08:00
JonnyWong16
823c9b3159 Format git reset release 2020-03-06 15:36:11 -08:00
JonnyWong16
35965a6a1b Add message that Tautulli will restart when resetting git branch 2020-03-06 15:30:47 -08:00
JonnyWong16
8d67cc4c5a Restart after resetting git install 2020-03-06 15:26:26 -08:00
JonnyWong16
42e33a0468 Reset git install to the release version 2020-03-06 15:25:52 -08:00
JonnyWong16
b2529db026 Fix Live TV channel logo cut off in popover 2020-03-05 20:58:32 -08:00
JonnyWong16
e99c4aec4a Substitute after matching hyphenated ip in logs 2020-03-03 20:36:09 -08:00
JonnyWong16
2d0a97f259 Add hyphenated IP address to log filter 2020-03-03 20:20:53 -08:00
JonnyWong16
fabb52763b Fix graphs not loading when history grouping is disabled 2020-03-03 14:00:55 -08:00
JonnyWong16
03dd1a6974 Add note to repair git install setting 2020-03-03 14:00:42 -08:00
JonnyWong16
ecee50a5e4 Add option to change library background art 2020-03-03 12:24:05 -08:00
JonnyWong16
cbab7c4cbf Add clip to notification posters 2020-03-03 10:58:50 -08:00
JonnyWong16
257ea14c59 Add Plex background art to notification parameters 2020-03-03 10:51:27 -08:00
JonnyWong16
11299291b0 Remove ipaddr js XHR 2020-03-03 09:38:22 -08:00
JonnyWong16
f63c1c2f7f Merge branch 'nightly' into python3 2020-03-02 10:12:28 -08:00
JonnyWong16
533b8076e4 Merge pull request #1366 from felixbuenemann/fix-ipv6-display-in-activity
Fix display of IPv6 addresses in activity panes
2020-03-02 10:11:42 -08:00
Felix Bünemann
b0383b4813 Fix display of IPv6 addresses in activity panes
* Fix wrong width of ip-address field, causing it to be displayed as
"…", when exceeding 125px
* Add tooltip with full IP address on hover, if the address is longer
than 20 characters (maximum length that can be shown in 125px)
2020-03-01 16:19:17 +01:00
JonnyWong16
5045e406a1 Update urllib.parse imports 2020-02-29 15:33:30 -08:00
JonnyWong16
8d5bc88fd9 Merge branch 'nightly' into python3
# Conflicts:
#	data/interfaces/default/current_activity_instance.html
#	plexpy/activity_handler.py
#	plexpy/graphs.py
#	plexpy/helpers.py
#	plexpy/pmsconnect.py
#	plexpy/version.py
#	plexpy/webserve.py
2020-02-29 15:26:33 -08:00
JonnyWong16
96500f75b0 Add option to reset Tautulli installation 2020-02-29 14:47:45 -08:00
JonnyWong16
2a3bd3413f Add --ff-only to git pull command 2020-02-29 14:10:20 -08:00
JonnyWong16
8ec136a0ca Fix creating metadata cache when starting session 2020-02-29 13:54:52 -08:00
JonnyWong16
2bead0fc29 Add live tv to log message 2020-02-29 13:54:24 -08:00
JonnyWong16
359776d48a Default webhook notification method to POST 2020-02-28 11:27:03 -08:00
JonnyWong16
05a16bb199 Fix tag in Docker release build workflow 2020-02-27 15:26:52 -08:00
JonnyWong16
f457704105 v2.2.0-beta 2020-02-27 14:53:29 -08:00
JonnyWong16
5aa59b93b0 Update Docker build workflows 2020-02-27 14:53:29 -08:00
JonnyWong16
47d51f92c7 Hide Live TV library from Library Statistics setting 2020-02-25 18:00:02 -08:00
JonnyWong16
068cf17d0a Only show graph series if the library type is present 2020-02-25 16:11:43 -08:00
JonnyWong16
3c1b849a5d Notify Discord on Docker build status 2020-02-25 11:26:27 -08:00
JonnyWong16
8384ec7f15 Add release workflows 2020-02-24 21:08:55 -08:00
JonnyWong16
0e7a3962bb Remove print 2020-02-24 16:20:31 -08:00
JonnyWong16
3d1bb9975c Fix interlaced video media flag on info page 2020-02-24 16:20:01 -08:00
JonnyWong16
eed473be15 Update README.md 2020-02-24 13:42:27 -08:00
JonnyWong16
e23d9bca22 Update README.md 2020-02-24 13:30:58 -08:00
JonnyWong16
85a952a5c9 Merge pull request #1364 from pkoenig10/manifest
Add crossorigin use-credentials attribute to manifest tag
2020-02-24 13:09:10 -08:00
JonnyWong16
5cc36310ba Change repo in workflow files 2020-02-24 12:18:23 -08:00
TheMeanCanEHdian
8690d2ced5 Disable updates when using Docker container 2020-02-24 10:26:52 -08:00
TheMeanCanEHdian
f572943a7b Add items needed to create Docker image 2020-02-24 10:26:50 -08:00
JonnyWong16
89248ad46a Fix typo 2020-02-23 09:35:52 -08:00
JonnyWong16
517db71916 Update API docs 2020-02-22 23:49:48 -08:00
JonnyWong16
ad82de010d Always have channel info in metadata response 2020-02-22 23:15:45 -08:00
JonnyWong16
02fd9edbbf Add value == 1 to helper.bool_true 2020-02-22 22:49:34 -08:00
JonnyWong16
6c84dd7be8 Clean up get_history filter parameters 2020-02-22 22:46:23 -08:00
JonnyWong16
9edbe6af37 Add helper to cast API parameter true to bool 2020-02-22 22:33:49 -08:00
JonnyWong16
37d09e9bad Check Plex guid before retrieving Live TV cloud metadata 2020-02-22 22:09:39 -08:00
JonnyWong16
0d0595e9a0 Add Live TV library icon 2020-02-22 17:21:22 -08:00
JonnyWong16
17477455cb Fix history table modal header for Live TV 2020-02-22 17:20:29 -08:00
JonnyWong16
43f594709d Don't include Live TV with Movies / TV Shows history filter 2020-02-22 17:09:31 -08:00
JonnyWong16
8885b3e7e0 Align library / user name 2020-02-22 16:53:41 -08:00
JonnyWong16
c060638539 Add Live TV to graphs 2020-02-22 16:31:35 -08:00
JonnyWong16
2144d4d7ed Change colours to Plex colours 2020-02-22 14:52:25 -08:00
JonnyWong16
4f2e09d733 Add link to Live TV library on info page 2020-02-22 14:09:04 -08:00
JonnyWong16
9d13c29bf6 Revert "Fix retrieving metadata for info page for Live TV without section_id"
This reverts commit 6fce31e1b9.
2020-02-22 14:08:49 -08:00
JonnyWong16
60d577f95e Revert "Add setting to enable Live TV logging"
This reverts commit 5d1bc3cf9b.
2020-02-22 14:02:34 -08:00
JonnyWong16
1dd1c6f67f Add fake Live TV library 2020-02-22 14:02:03 -08:00
JonnyWong16
bcd0691b33 Fix Live TV poster fallback for notifications 2020-02-22 13:11:27 -08:00
JonnyWong16
24a2559ab5 Fix channel popover on info page 2020-02-22 13:10:59 -08:00
JonnyWong16
22a6bae4cf Add new Live TV art fallback for info page 2020-02-22 12:42:40 -08:00
JonnyWong16
2c45de1fe5 Add Live TV channel notification parameters 2020-02-22 12:12:45 -08:00
JonnyWong16
0ab93d7a7f Fix Live TV roll over metadata caching 2020-02-22 11:39:27 -08:00
JonnyWong16
c8831efb28 Update activity card on Live TV rollover 2020-02-20 20:51:05 -08:00
JonnyWong16
0cbde5a2f5 Fix ambiguous column name in Live TV session grouping 2020-02-20 19:10:12 -08:00
JonnyWong16
7de82d87f7 Update history grouping for Live TV only if the user's last session has a matching guid 2020-02-20 18:59:12 -08:00
JonnyWong16
751b97a39c Stop Live TV session when guid changes 2020-02-20 18:58:36 -08:00
JonnyWong16
7b58bcc279 Check for metadata returned from metadata.provider.plex.tv 2020-02-20 13:18:47 -08:00
JonnyWong16
820a2e688c Add channel icon popover to info page 2020-02-20 12:30:54 -08:00
JonnyWong16
10a7f540ad Fix filtering history using guid 2020-02-20 12:10:51 -08:00
JonnyWong16
31a6c627af Fix incorrect source=history in info page URL for active sessions on history table 2020-02-20 11:30:17 -08:00
JonnyWong16
aba4cbf9e4 Write guid to session table 2020-02-20 11:20:29 -08:00
JonnyWong16
a5624e86e4 Replace single quotes in page query params 2020-02-20 11:09:33 -08:00
JonnyWong16
5480d09a0b Add library and user page helper functions 2020-02-20 10:13:27 -08:00
JonnyWong16
5fad0a1d97 Refactor page helper functions 2020-02-20 09:00:10 -08:00
JonnyWong16
4a7b5bab54 Update all info page links with helper function 2020-02-20 00:01:30 -08:00
JonnyWong16
fe0557dcc1 Add helper function for info page 2020-02-20 00:00:04 -08:00
JonnyWong16
e4b6d61098 Add option to retrieve history using guid 2020-02-19 23:57:55 -08:00
JonnyWong16
73e01ebaaf Use pms_image_proxy helper function for datatables 2020-02-19 21:19:44 -08:00
JonnyWong16
d752d46676 Update pms_image_proxy with helper function and encode URL params 2020-02-19 19:57:56 -08:00
JonnyWong16
3932409fb4 Fix info page redirect with invalid rating key 2020-02-18 15:55:27 -08:00
JonnyWong16
f69d4f1c42 Fix typo in activity bandwidth header 2020-02-18 15:30:02 -08:00
JonnyWong16
96699fc3b0 Remove debug logging 2020-02-18 08:52:19 -08:00
JonnyWong16
97ec8f6828 Add some debug logging 2020-02-18 08:43:48 -08:00
JonnyWong16
df851e67f9 Get additional Live TV metadata from metadata.provider.plex.tv 2020-02-17 20:52:16 -08:00
JonnyWong16
5d1bc3cf9b Add setting to enable Live TV logging 2020-02-17 17:53:13 -08:00
JonnyWong16
81ab9b006d Add Live TV history filter button 2020-02-17 17:41:28 -08:00
JonnyWong16
1699fc09cf Get history using title and year for Live TV info page 2020-02-17 17:32:52 -08:00
JonnyWong16
f8b6a9f1e8 Fix showing Gbps bandwidth on activity card 2020-02-17 17:08:12 -08:00
JonnyWong16
ba465a0d15 Move live key to main metadata dict 2020-02-17 16:11:26 -08:00
JonnyWong16
af521b4058 Fix air date for active sessions on history table 2020-02-17 15:59:02 -08:00
JonnyWong16
27b512611f Change channel popover background and enlarge 2020-02-17 15:57:15 -08:00
JonnyWong16
b79f165a27 More fix for poster/background not showing for Live TV watch statistics 2020-02-17 15:53:56 -08:00
JonnyWong16
716f76baed Fix poster/background not showing for Live TV watch statistics 2020-02-17 15:23:36 -08:00
JonnyWong16
18c57a4fc6 Fix recently watched on user page 2020-02-17 15:04:38 -08:00
JonnyWong16
c26483b4b8 Add channel thumb popover back onto activity card 2020-02-17 13:44:03 -08:00
JonnyWong16
441e39d776 Add air date to tables for non-episodic Live TV 2020-02-17 13:37:58 -08:00
JonnyWong16
7ecc075c7e Correct rating_key link for watch statistics 2020-02-17 12:44:47 -08:00
JonnyWong16
2e42663832 Fix watch statistic links for Live TV 2020-02-17 12:42:00 -08:00
JonnyWong16
075f9f8cbd Fallback to Live TV art/poster for watch statistics 2020-02-17 12:06:24 -08:00
JonnyWong16
f3d42e7b53 Show Gbps bandwidth 2020-02-17 12:00:00 -08:00
JonnyWong16
fa0b547b32 Fix library recenly watched missing live key 2020-02-17 10:51:44 -08:00
JonnyWong16
8f19cdccfd Add rating_key to all uses of pms_image_proxy 2020-02-17 10:51:18 -08:00
JonnyWong16
5c207aeee6 Fix Live TV shown on user page and user tables 2020-02-16 21:04:33 -08:00
JonnyWong16
d48273ef98 Fix "Live TV" in activity card subtitle for episodic tv 2020-02-16 20:43:48 -08:00
JonnyWong16
a7803dcad7 Fix Live TV air date on info page 2020-02-16 20:36:53 -08:00
JonnyWong16
4fc9b6fdb4 More Live TV fixes for info page 2020-02-16 20:13:27 -08:00
JonnyWong16
73f8f83658 Fallback pms_image_proxy when no image imput received 2020-02-16 20:06:35 -08:00
JonnyWong16
66a0f953b3 Fix missing channel info for Live TV history on info page 2020-02-16 19:56:27 -08:00
JonnyWong16
f189eea32b Use Live TV added_at date to fake air date 2020-02-16 19:53:51 -08:00
JonnyWong16
be29d879a7 Fix missing title on info page for Live TV episodes 2020-02-16 19:39:43 -08:00
JonnyWong16
e3e906c9e5 Remove click through link to Plex Web on info page for Live TV 2020-02-16 19:37:58 -08:00
JonnyWong16
6fce31e1b9 Fix retrieving metadata for info page for Live TV without section_id 2020-02-16 19:33:40 -08:00
JonnyWong16
db1cb3d658 Forgot saving channel info to sessions db table 2020-02-16 19:07:08 -08:00
JonnyWong16
c0d7c5ddff Disable Recently Added Notification button on info page for Live TV 2020-02-16 19:05:41 -08:00
JonnyWong16
d0cd2672dd Fix Live TV artwork on activity card 2020-02-16 19:02:29 -08:00
JonnyWong16
ca55900d40 Update info page for Live TV 2020-02-16 16:23:58 -08:00
JonnyWong16
3d65ffc6d3 Update font-awesome icon for Live TV 2020-02-16 16:23:42 -08:00
JonnyWong16
f94b796c2b Fix saving channel info to database 2020-02-16 16:06:43 -08:00
JonnyWong16
66c1fd6593 Save Live TV sessions to the database 2020-02-16 14:54:48 -08:00
JonnyWong16
13a45facf9 Update pms_image_proxy to work with web URLs 2020-02-15 20:14:50 -08:00
JonnyWong16
26f10b2c3d Add poster to activity card for Live TV movies 2020-02-15 14:36:51 -08:00
JonnyWong16
9aea4c85b0 Remove test parameters from Live TV 2020-02-15 14:28:19 -08:00
JonnyWong16
ee1b0eeeff Add Live TV metadata to activity card 2020-02-15 13:29:25 -08:00
JonnyWong16
c6cfb4a020 More Live TV fixes 2020-02-15 11:44:11 -08:00
JonnyWong16
91da41ff86 Fix Live TV on PMS 1.18.7.2415 crashing Tautulli activity 2020-02-15 10:48:01 -08:00
JonnyWong16
a811edb236 Fix race condition for notification stream count when stopping playback 2020-02-15 10:03:11 -08:00
Patrick Koenig
66de806b02 Add crossorigin use-credentials attribute to manifest tag 2020-02-14 08:11:07 -08:00
JonnyWong16
c825176563 v2.1.44 2020-02-05 21:41:31 -08:00
JonnyWong16
7ae87fe0e7 Fix SDR source video being identified as HDR stream video 2020-02-05 21:37:33 -08:00
JonnyWong16
99f826c236 Fix video color notification condition types 2020-02-05 20:24:43 -08:00
JonnyWong16
f88d673e87 Remove capitalization for platform on history tables 2020-02-04 13:48:29 -08:00
JonnyWong16
bc2f73d686 v2.1.43 2020-02-03 18:27:41 -08:00
JonnyWong16
645c2ecef6 Use Authorization header for GitHub API 2020-02-03 18:11:56 -08:00
JonnyWong16
4d4a8ca3b2 Trim MaxMind License Key when saving 2020-01-28 10:38:21 -08:00
JonnyWong16
a98bc45c10 Fix HDR for direct stream video 2020-01-24 20:14:48 -08:00
aaronldunlap
aa5affe366 Change humanFileSize to default to SI notation 2020-01-23 17:09:39 -06:00
JonnyWong16
b39ac866f2 Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/webserve.py
2020-01-20 20:55:30 -08:00
JonnyWong16
b9d5e49a71 Make verbose logging an advanced config option (Fixes Tautulli/Tautulli-Issues#205)
* Verbose logging is enabled by default
* Make toggleVerbose stick
2020-01-20 20:49:37 -08:00
JonnyWong16
e90390be67 Fix plural episodes and albums in newsletter templates (Fixes Tautulli/Tautulli-Issues#203) 2020-01-20 20:08:45 -08:00
JonnyWong16
8ef671c9cb Fix Telegram notification character limit (Closes #1361) 2020-01-20 19:56:38 -08:00
JonnyWong16
8829516cae Fix season 0 number not showing in tables 2020-01-20 19:23:16 -08:00
JonnyWong16
4c211342a2 Remove string encoding from notifiers 2020-01-19 17:09:50 -08:00
JonnyWong16
6b7cd38d71 Merge branch 'nightly' into python3 2020-01-19 16:43:53 -08:00
JonnyWong16
d8e8dfbd45 Revert "Match session user using user_id instead of username"
This reverts commit 6380de3e6c.
2020-01-19 16:43:34 -08:00
JonnyWong16
485609fbb9 Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/__init__.py
#	plexpy/helpers.py
#	plexpy/logger.py
#	plexpy/version.py
2020-01-19 16:40:19 -08:00
JonnyWong16
0b6d9a4890 Add email address log filter 2020-01-19 16:03:10 -08:00
JonnyWong16
55ffd68023 Refactor log filter 2020-01-19 16:02:56 -08:00
JonnyWong16
6380de3e6c Match session user using user_id instead of username 2020-01-19 16:00:36 -08:00
JonnyWong16
d6220a921a Log dynamic range to history and show in stream info modal 2020-01-18 11:46:43 -08:00
JonnyWong16
10e421b9d4 Add color and dynamic range to notification parameters 2020-01-15 12:19:43 -08:00
JonnyWong16
90056bcce2 Indicate HDR content on activity cards 2020-01-15 11:38:50 -08:00
JonnyWong16
4740d0fbf3 Change webhook request data if not Content-Type header is not application/json 2020-01-14 17:36:25 -08:00
JonnyWong16
13374c9ded Fix Windows and macOS platform capitalization 2020-01-14 17:35:35 -08:00
JonnyWong16
2a03be1905 Fix typo in CHANGELOG.md 2020-01-04 17:49:27 -08:00
JonnyWong16
c8f132a750 v2.1.42 2020-01-04 17:46:03 -08:00
JonnyWong16
4d0c4bf4f4 Update GeoLite2 database help text to leave blank for default 2020-01-04 12:03:44 -08:00
JonnyWong16
e321c5b197 Fix GeoLite2 update log message to correct number of days from setting 2020-01-04 11:50:48 -08:00
JonnyWong16
badbfdc4c1 Disable uninstall GeoLite2 database button when not installed 2020-01-04 10:52:11 -08:00
JonnyWong16
7d71086a41 Verify MaxMind license key and GeoLite2 database path before allowing install 2020-01-02 21:28:11 -08:00
JonnyWong16
0e1764755a Change save setting callbacks 2020-01-02 21:25:15 -08:00
JonnyWong16
c31d3ffd6c Download from MaxMind using urllib3 using certifi 2020-01-01 10:10:52 -08:00
JonnyWong16
0cba3988aa Update certifi to 2019.11.28 2020-01-01 10:10:05 -08:00
JonnyWong16
a7a9ed8628 v2.1.41 2019-12-30 14:33:10 -08:00
JonnyWong16
6af96332fa Use tar.extractall instead of tar.extract 2019-12-30 14:29:19 -08:00
JonnyWong16
e334a0fc8b Change GeoLite2 tar extraction 2019-12-30 14:19:12 -08:00
JonnyWong16
54bbbb36a6 Fix Geolite2 db extraction on Windows 2019-12-30 13:58:56 -08:00
JonnyWong16
b8ef56574a v2.1.40 2019-12-30 10:24:46 -08:00
JonnyWong16
bc491628d4 Add links to 3rd Party APIs guide 2019-12-29 15:03:22 -08:00
JonnyWong16
629800c239 Change metadata to links for lookup help text 2019-12-29 11:05:21 -08:00
JonnyWong16
8bf876f88c Add note to save the MaxMind license key 2019-12-28 11:09:52 -08:00
JonnyWong16
a81ad6d73e Set maximum GeoLite2 database update interval to 30 days 2019-12-28 11:06:04 -08:00
JonnyWong16
02220209e3 Add default activity refresh interval to setting help text 2019-12-27 09:48:38 -08:00
JonnyWong16
9450a1434d Remove line break from Python version in startup logs 2019-12-25 14:10:35 -08:00
JonnyWong16
c358693fb2 Fix GeoLite2 update scheduler 2019-12-25 14:10:16 -08:00
JonnyWong16
e7b3d768ce Prevent installing GeoLite2 database without license key 2019-12-25 14:07:59 -08:00
JonnyWong16
ee91da2ff1 Force reinstall of GeoLite2 database 2019-12-25 13:43:12 -08:00
JonnyWong16
0428df8e3f Fix GeoLite2 update button for previous installs 2019-12-25 13:31:46 -08:00
JonnyWong16
d4fee1d701 Fix GeoLite2 last updated time for previous installs 2019-12-25 13:23:33 -08:00
JonnyWong16
3b44a3afd2 Add setting for GeoLite2 database update interval 2019-12-25 12:51:59 -08:00
JonnyWong16
7ee1c51810 Go to correct setting for installing GeoLite2 database 2019-12-25 12:40:58 -08:00
JonnyWong16
f958de2de6 Move 3rd Party API settings to new tab 2019-12-25 11:41:53 -08:00
JonnyWong16
b83eb2e763 Add auto-updating of GeoLite2 database 2019-12-25 11:17:45 -08:00
JonnyWong16
41c9369b43 Change GeoIP database install using license key 2019-12-23 23:58:36 -08:00
JonnyWong16
554be92c39 Update go to setting links 2019-12-23 22:30:49 -08:00
JonnyWong16
a44709a43d Cast section_id to int in websocket timeline data 2019-12-12 18:44:55 -08:00
JonnyWong16
65e9e2b680 Remove graph months str decode 2019-12-12 09:14:34 -08:00
JonnyWong16
d84dc23b46 Merge branch 'nightly' into python3 2019-12-12 09:13:16 -08:00
JonnyWong16
7486a50c33 Calculate months for graphs using datetime 2019-12-12 09:12:29 -08:00
JonnyWong16
e333940826 Set log files to UTF-8 2019-12-12 08:58:57 -08:00
JonnyWong16
70f7fd2de9 Change dict iteritems to items for HTML templates 2019-12-12 08:58:34 -08:00
JonnyWong16
411d88d798 Merge branch 'nightly' into python3
# Conflicts:
#	plexpy/notification_handler.py
2019-12-11 11:39:59 -08:00
JonnyWong16
1e777b1a1b Add logging for notify_action (trigger) 2019-12-11 11:35:30 -08:00
JonnyWong16
b7bb159630 Fix regex IP match in hostname resolution 2019-12-10 10:07:34 -08:00
JonnyWong16
dce8248eb8 Change children_count to int 2019-12-08 12:49:48 -08:00
JonnyWong16
3b8234ce67 Use six.moves.urllib 2019-12-08 12:49:09 -08:00
JonnyWong16
ac63d3c3ce Fix version.py 2019-12-08 12:28:39 -08:00
JonnyWong16
197c3a327b Merge branch 'nightly' into python3 2019-12-08 12:20:43 -08:00
JonnyWong16
6e0a0d51b5 v2.1.39 2019-12-08 12:12:24 -08:00
JonnyWong16
55aad4e6ee Fix integrity check before database backup 2019-11-29 23:14:33 -08:00
JonnyWong16
d1e401cb0c Remove default subject and body for notify API command 2019-11-24 22:35:58 -08:00
JonnyWong16
0bb97fee31 Encode request message to UTF-8 2019-11-24 16:27:26 -08:00
JonnyWong16
1bdf6bbb66 Encode image hash before converting to hex 2019-11-24 15:59:56 -08:00
JonnyWong16
077dfe7164 Decode jwt_token for login cookie 2019-11-24 15:50:27 -08:00
JonnyWong16
169f83ac4a Update hashing_passwords to use hashlib and remove pbkdf2 2019-11-24 15:49:17 -08:00
JonnyWong16
121dad588e Merge branch 'nightly' into python3 2019-11-24 15:03:58 -08:00
JonnyWong16
018479fae9 Fix favicons in setup wizard 2019-11-24 15:03:36 -08:00
JonnyWong16
bb3a11ad00 Temporarily set environment to test_suite 2019-11-24 14:55:16 -08:00
JonnyWong16
64d3bd9c4f Temporarily disable analytics for python3 branch 2019-11-24 14:52:24 -08:00
JonnyWong16
e6be03a770 Use bytearray for pbkdf2 2019-11-24 14:50:30 -08:00
JonnyWong16
5f722570d2 Encode request data in UniversalAnalytics to UTF-8 2019-11-24 14:41:49 -08:00
JonnyWong16
dcbeca5f7f Encode uuid before hashing in UniversalAnalytics 2019-11-24 14:18:58 -08:00
JonnyWong16
16742d4705 Patch ipwhois literal comparison 2019-11-24 11:32:11 -08:00
JonnyWong16
d21a03905d Add soupsieve-1.9.5 2019-11-24 11:28:02 -08:00
JonnyWong16
0608b2a1df Patch UniversalAnalytics with 2to3 2019-11-24 11:27:19 -08:00
JonnyWong16
5f237c7c71 Fix starting cherrypy server 2019-11-23 19:21:40 -08:00
JonnyWong16
4c98b0a43d Remove NotifyMyAndroid and Pushalot 2019-11-23 19:21:30 -08:00
JonnyWong16
05afa0859c Run futurize --unicode-literals 2019-11-23 19:21:10 -08:00
JonnyWong16
597cc9fe29 Run futurize --stage2 2019-11-23 19:16:51 -08:00
JonnyWong16
ab6196589b Run futurize --stage1 2019-11-23 19:11:42 -08:00
JonnyWong16
221be380ee Remove pynma 2019-11-23 19:05:07 -08:00
JonnyWong16
a68e5f6519 Add zc.lockfile-2.0 2019-11-23 19:04:25 -08:00
JonnyWong16
bc81f19715 Add tempora-1.14.1 2019-11-23 19:04:08 -08:00
JonnyWong16
ceeeea94ba Add more_itertools-5.0.0 2019-11-23 19:03:48 -08:00
JonnyWong16
31ab5daa91 Add jaraco.functools-2.0 2019-11-23 19:03:22 -08:00
JonnyWong16
8f6639028f Add cheroot-8.2.1 2019-11-23 19:03:04 -08:00
JonnyWong16
a2b686f6df Add backports.functools_lru_cache-1.6.1 2019-11-23 19:02:44 -08:00
JonnyWong16
2dcc74d82d Add tokenize_rt-3.2.0 2019-11-23 19:02:18 -08:00
JonnyWong16
d460263b97 Add sgmllib3 2019-11-23 19:01:56 -08:00
JonnyWong16
b8cfa343ae Add portend-2.6 2019-11-23 19:01:14 -08:00
JonnyWong16
8d391f125c Add future_fstrings-1.2.0 2019-11-23 19:01:00 -08:00
JonnyWong16
1532bb731a Add distro-1.4.0 2019-11-23 19:00:47 -08:00
JonnyWong16
357ba9ec59 Add contextlib2-0.6.0 2019-11-23 19:00:36 -08:00
JonnyWong16
183c810c76 Update configobj to 5.1.0 2019-11-23 18:57:54 -08:00
JonnyWong16
f2d7beec90 Update mako to 1.1.0 2019-11-23 18:57:21 -08:00
JonnyWong16
84ce4758d1 Update ipwhois to 1.1.0 2019-11-23 18:55:41 -08:00
JonnyWong16
4d6279a626 Update cherrpy to 17.4.2 2019-11-23 18:55:19 -08:00
JonnyWong16
f28e741ad7 Update bs4 to 4.8.1 (with 2to3) 2019-11-23 18:54:24 -08:00
JonnyWong16
23c4e5b09d Update pbkdf2 with 2to3 2019-11-23 18:51:02 -08:00
JonnyWong16
cd6057e1ca Update hashing_passwords with 2to3 2019-11-23 18:50:49 -08:00
JonnyWong16
1771674b53 Update feedparser to 5.2.1 (with 2to3) 2019-11-23 18:50:34 -08:00
JonnyWong16
2a9d0ea7d2 Update argparse to 1.4.0 2019-11-23 18:50:16 -08:00
JonnyWong16
e19938b05e Patch UniversalAnalytics using six 2019-11-23 16:14:37 -08:00
JonnyWong16
244a3e5be3 Update apscheduler to version 3.6.3 2019-11-23 14:38:11 -08:00
JonnyWong16
e5a3d534b2 Update six to version 1.13.0 2019-11-23 14:37:41 -08:00
JonnyWong16
c279057f91 Remove unicode strings 2019-11-23 14:37:26 -08:00
JonnyWong16
1c18e72539 Enable allow Plex admin by default 2019-11-20 21:51:52 -08:00
JonnyWong16
779e710045 Fix setup wizard height 2019-11-20 21:51:36 -08:00
JonnyWong16
089a981f6e Check for database corruption when making backup 2019-11-20 19:03:44 -08:00
JonnyWong16
3b24bbee5f Add web server HTTP username and password to setup wizard 2019-11-20 18:50:17 -08:00
JonnyWong16
f9a597bed9 Use bit.ly link for PayPal in FUNDING file 2019-11-17 21:54:47 -08:00
JonnyWong16
a637b3bb24 v2.1.38 2019-11-17 21:45:43 -08:00
JonnyWong16
3e520820d8 Update pytz to 2019.3 2019-11-17 21:13:21 -08:00
JonnyWong16
3a71929821 Remove issues template 2019-11-13 19:44:33 -08:00
JonnyWong16
a08629c503 Add GitHub sponsor 2019-11-13 19:40:26 -08:00
JonnyWong16
cfc30c1234 Add optional headers parameter for webhooks in notify API command 2019-11-11 15:41:11 -08:00
JonnyWong16
ddbd486500 Add custom headers to Webhook notification agent 2019-11-11 15:17:49 -08:00
JonnyWong16
f5794a5bae Improve resolving hostnames 2019-11-02 10:38:14 -07:00
JonnyWong16
bb1bf87fe2 Fix homepage recently watched not showing grouped history 2019-10-27 16:06:19 -07:00
JonnyWong16
acc59523e0 Prevent dismissing the changelog modal 2019-10-11 22:55:23 -07:00
JonnyWong16
38d7ea16b4 Fix typo in changelogs 2019-10-11 20:37:26 -07:00
JonnyWong16
6e3147c5f5 v2.1.37 2019-10-11 20:26:54 -07:00
JonnyWong16
1b09f225ff Remove blank line 2019-10-11 20:14:25 -07:00
JonnyWong16
3cf8c4f8a8 Fix rare case when HTTP_ROOT is None when retrieving URL 2019-10-11 20:11:13 -07:00
JonnyWong16
30be4b473f Log individual custom conditions 2019-10-11 19:54:48 -07:00
JonnyWong16
6908034a86 Add ability to delete MusicBrainz lookup info 2019-10-06 16:50:26 -07:00
JonnyWong16
cba43f675a Save MusicBrainz lookups in the database 2019-10-06 15:16:19 -07:00
JonnyWong16
6ff826bc3a Fix Last.fm URLs linking to the artist page instead of the album page 2019-10-05 23:58:17 -07:00
JonnyWong16
c7afd10ec0 Add notification parameters for MusicBrainz lookup 2019-10-05 23:57:48 -07:00
JonnyWong16
b39d5174f9 Add 3rd party MusicBrains lookup setting 2019-10-05 23:44:13 -07:00
JonnyWong16
501bc0ab3f Add musicbrainzngs v0.7dev 2019-10-05 23:27:16 -07:00
JonnyWong16
688d28b5ea Limit Discord description to 2048 characters 2019-10-05 23:26:41 -07:00
JonnyWong16
27d2c7b078 v2.1.36-beta 2019-10-05 21:59:14 -07:00
JonnyWong16
2fb12ccf65 Fix updating activity card when rating key changes for the same session key (Fixes Tautulli/Tautulli-Issues#96) 2019-10-05 21:48:11 -07:00
JonnyWong16
cb92d159c1 Add parent_guid and grandparent_guid to get_activity and get_metadata API commands 2019-10-05 20:21:15 -07:00
JonnyWong16
64bdf4237c Separate progressive vs. interlaced video on stream type graphs 2019-09-27 13:44:08 -07:00
JonnyWong16
fd7b4ec7e3 Get srouce video_full_resolution from Plex metadata 2019-09-27 13:26:55 -07:00
JonnyWong16
57eb57d4d7 Use video_full_resolution in stream data modal 2019-09-27 13:04:40 -07:00
JonnyWong16
7974e9505b Save session video_scan_type and video_full_resolution to the database 2019-09-27 13:04:17 -07:00
JonnyWong16
7498fb37b5 Update database with video_scan_type and video_full_resolution 2019-09-27 13:03:34 -07:00
JonnyWong16
2cc3e88e6c Fix Albums typo on media info table 2019-09-26 20:14:48 -07:00
JonnyWong16
5fd8cfeb80 v2.1.35-beta 2019-09-24 18:09:05 -07:00
JonnyWong16
b295566a4e Clear metadata cache before retrieving new sessions 2019-09-19 21:01:37 -07:00
JonnyWong16
e0943a2d55 Clear metadata cache on startup check for active sessions 2019-09-19 20:55:19 -07:00
JonnyWong16
3015740c3e Merge pull request #1354 from samwiseg0/fix/blank_audio
Fix blank audio on activity card when changing audio streams during direct play
2019-09-19 19:40:14 -07:00
JonnyWong16
ec9ff2f803 Merge pull request #1353 from samwiseg0/fix/video_scan_type
Add video scan type and standardize video resolution. Fixes Tautulli/Tautulli-Issues#194
2019-09-19 19:39:40 -07:00
JonnyWong16
ec8aae9122 Fix Tautulli logging out after saving settings and restarting 2019-09-19 19:37:31 -07:00
samwiseg0
52e608cc43 Fix blank audio on activity card when changing audio streams 2019-09-19 22:29:44 -04:00
samwiseg0
8213f270e5 Move full resolution outside if statement 2019-09-19 22:26:43 -04:00
JonnyWong16
7085042b0d Merge pull request #1352 from samwiseg0/fix/watch_stats
Add year to SQL query for movie watch stats
2019-09-19 19:04:06 -07:00
JonnyWong16
6a411d2458 Merge pull request #1351 from samwiseg0/fix/release_date
Fix release date to be a string vs integer
2019-09-19 19:03:57 -07:00
JonnyWong16
38e2fbabb8 Merge pull request #1350 from samwiseg0/fix/lin_hw_decode
Fix hardware decode not reflected in GUI
2019-09-19 19:03:48 -07:00
samwiseg0
85709f754a Update API docs to reflect changes 2019-09-19 21:22:37 -04:00
samwiseg0
623a1e8a91 Update webui to utilize video full resolution 2019-09-19 21:22:02 -04:00
samwiseg0
de69945ebe Define notification parameters 2019-09-19 20:57:08 -04:00
samwiseg0
7095fa6ac6 Remove overrides 2019-09-19 20:53:18 -04:00
samwiseg0
a59e8298fd Use video_full_resolution for optimized versions in the activity card 2019-09-19 20:50:17 -04:00
samwiseg0
2737d52279 Set the full resolution of the source video and stream video 2019-09-19 20:47:39 -04:00
samwiseg0
0ac1ad4386 Create video_scan_type and stream_video_scan_type 2019-09-19 20:46:38 -04:00
samwiseg0
2db328ac31 Standardize videoResolution to be lowercase 2019-09-19 20:45:53 -04:00
samwiseg0
b6de4ad054 Add year to SQL query watch stats. Fixes Tautulli/Tautulli-Issues#195 2019-09-16 23:44:48 -04:00
samwiseg0
cfea7164b7 Fix release date to be a str vs int 2019-09-16 22:50:31 -04:00
samwiseg0
7e7e5a6be4 Add nvdec for linux decode support. Fixes Tautulli/Tautulli-Issues#193 2019-09-16 21:30:09 -04:00
samwiseg0
df57f4c009 Remove duplicate nvenc 2019-09-16 21:24:57 -04:00
JonnyWong16
c2185c4ce5 Fix notificaiton parameter prefix and suffix not being substituted correctly 2019-09-07 16:37:38 -07:00
JonnyWong16
08714436c3 v2.1.34 2019-09-03 21:46:48 -07:00
JonnyWong16
f65f5d07c0 Add product to get_history API command 2019-09-03 19:52:54 -07:00
JonnyWong16
a9b10c4560 Add Product column to history tables 2019-09-03 19:52:33 -07:00
JonnyWong16
589fbd3158 Add TVMaze and TMDB IDs to notification parameters after lookup 2019-08-27 19:54:54 -07:00
JonnyWong16
0ffc8c5d19 Update JWT secret instead of UUID 2019-08-24 22:00:31 -07:00
JonnyWong16
7498617b74 Flag update JWT UUID after restarting 2019-08-24 21:13:39 -07:00
JonnyWong16
f21d505ab8 Force logout all clients when changing the admin password 2019-08-24 20:49:56 -07:00
JonnyWong16
7b16af0585 Fix verifying PMS with unpublished hostnames (Fixes Tautulli/Tautulli-Issues#190) 2019-08-16 21:27:34 -07:00
JonnyWong16
a83108282a Fix add title to searchable media info fields 2019-08-14 22:50:23 -07:00
JonnyWong16
1c4d01d6ec Fix libraries/users table respect grouping setting 2019-08-09 19:15:53 -07:00
JonnyWong16
22e6d4067d Missing space for episode titles on tables 2019-08-09 19:01:27 -07:00
JonnyWong16
1046b29c1a v2.1.33 2019-07-27 08:44:26 -07:00
JonnyWong16
d6127e28f3 Remove email sort 2019-07-22 09:03:20 -07:00
JonnyWong16
25a949356d Allow seraching by email address in dropdown menu 2019-07-21 09:46:17 -07:00
JonnyWong16
72a012b817 Update Plex OAuth headers 2019-07-18 11:39:16 -07:00
JonnyWong16
f439bd639c Make sure config has name and value when masking passwords 2019-07-10 21:37:32 -07:00
JonnyWong16
91476a420a Mask notifier and newsletter config passwords (Fixes Tautulli/Tautulli-Issues#172) 2019-07-08 23:50:48 -07:00
JonnyWong16
96c0f9cad5 Add if Docker container to platform 2019-07-08 22:30:58 -07:00
JonnyWong16
df50559495 Fix overwriting version number with None 2019-07-02 08:58:56 -07:00
JonnyWong16
6d35bd7947 v2.1.32 2019-06-26 19:14:30 -07:00
JonnyWong16
d27356bbba Fix timezone error with newsletter scheduler because QNAP devices use a stupid "local" timezone (Fixes Tautulli/Tautulli-Issues#183) 2019-06-26 19:11:33 -07:00
JonnyWong16
3054a824ce v2.1.31 2019-06-24 21:45:57 -07:00
JonnyWong16
3b22b6a3f7 v2.1.31-beta 2019-06-13 22:14:42 -07:00
JonnyWong16
e4be5a716f Fix unable to view database status when auth is disabled 2019-06-13 21:57:05 -07:00
JonnyWong16
13579b8140 Change default database synchronous mode to normal 2019-06-09 10:48:59 -07:00
JonnyWong16
b11437b86b Fix synced content showing incorrect stream info 2019-05-12 17:30:04 -07:00
JonnyWong16
4b744a5acd v2.1.30-beta 2019-05-11 10:46:45 -07:00
JonnyWong16
db5be89710 Merge branch 'master' into nightly 2019-05-11 10:45:34 -07:00
JonnyWong16
46e77b074a Add database status link in configuration table 2019-05-11 10:28:53 -07:00
JonnyWong16
f1376fb2ca Add database integrity check to status API 2019-05-11 10:28:52 -07:00
JonnyWong16
2635569e76 v2.1.29 2019-05-11 09:10:37 -07:00
JonnyWong16
4743538de8 Add status API command 2019-05-11 09:02:18 -07:00
JonnyWong16
235d6f259e Fix missing comma in creating session_history table 2019-05-10 17:54:47 -07:00
JonnyWong16
049a30adbc Update certifi to 2019.03.09 2019-05-02 20:19:42 -07:00
JonnyWong16
e63606f195 Update pytz to 2019.1 2019-05-02 20:13:52 -07:00
JonnyWong16
eea1cf1d2c Schedule jobs using UTC to prevent tiggers in the past 2019-05-02 20:01:39 -07:00
JonnyWong16
43b350a1cc Improve timezone fallback 2019-05-02 20:00:32 -07:00
JonnyWong16
87a77f0522 Fix queue tables displaying intervals greater than 24 hours 2019-04-27 17:03:04 -07:00
JonnyWong16
ce4bfd56c2 Fix scheduler table displaying intervals greater than 24 hours 2019-04-27 16:58:23 -07:00
JonnyWong16
81d443f34c Reorder config 2019-04-27 16:13:25 -07:00
JonnyWong16
4c5602c77c Merge pull request #1336 from abiacco/pms_notify_interval
Adds ability to change notification/check interval for new plex server version
2019-04-27 16:10:49 -07:00
JonnyWong16
feab4115ee Add secure and relayed to notification parameters 2019-04-27 15:54:46 -07:00
JonnyWong16
eae842b09a Store websocket rating key to compare against when mismatched with sessions XML (Fixes Tautulli/Tautulli-Issues#174) 2019-04-27 15:48:31 -07:00
JonnyWong16
9b42bb240a Add secure and relayed to the database 2019-04-27 15:40:33 -07:00
JonnyWong16
d3a7229c00 Fix removing session metadata cache exception IOError to OSError 2019-04-27 10:32:56 -07:00
JonnyWong16
133335a3fb Check section_id > 0 when returning library details 2019-04-27 10:16:11 -07:00
JonnyWong16
32c05136bd Add PLEX_USER_TOKEN to script environment variables 2019-04-18 10:22:04 -07:00
Anthony Biacco
b8d4bb7d69 remove old notify_interval block 2019-04-15 13:45:32 -06:00
JonnyWong16
fc457585fb Update plexpy/webserve.py
Co-Authored-By: abiacco <abiacco@handll.com>
2019-04-15 13:08:04 -06:00
JonnyWong16
74c07167e1 Update plexpy/webserve.py
Co-Authored-By: abiacco <abiacco@handll.com>
2019-04-15 13:07:46 -06:00
JonnyWong16
beabd7bb0f Update plexpy/__init__.py
Co-Authored-By: abiacco <abiacco@handll.com>
2019-04-15 13:07:34 -06:00
JonnyWong16
9a6106f10e Update plexpy/config.py
Co-Authored-By: abiacco <abiacco@handll.com>
2019-04-15 13:07:14 -06:00
JonnyWong16
75949a8dd2 Update plexpy/__init__.py
Co-Authored-By: abiacco <abiacco@handll.com>
2019-04-15 13:07:07 -06:00
JonnyWong16
b92b057ab9 Update data/interfaces/default/settings.html
Co-Authored-By: abiacco <abiacco@handll.com>
2019-04-15 13:06:02 -06:00
JonnyWong16
7d11e1de2d v2.1.29-beta 2019-04-14 13:43:58 -07:00
JonnyWong16
36aa4a6be3 Update API docs 2019-04-13 23:14:41 -07:00
JonnyWong16
24ed63e07c Add undelete button to edit library/user modal 2019-04-13 22:56:32 -07:00
JonnyWong16
f41ed9953a Improve API response codes 2019-04-13 22:53:56 -07:00
JonnyWong16
6970231687 Log script return status code 2019-04-13 22:01:03 -07:00
JonnyWong16
ea036aa354 Fix typo in notifier config triggers 2019-04-13 18:05:40 -07:00
JonnyWong16
d0a7c2f92c Fix typo in notification parameters list 2019-04-10 18:32:54 -07:00
JonnyWong16
f07acd839b Missing closing div tag in c1fd798 2019-04-07 14:22:49 -07:00
JonnyWong16
c1fd798fe9 Add prefix and suffix text modifiers to notifications 2019-04-04 19:21:26 -07:00
JonnyWong16
2f8d2f23fe Add user IP table column IDs 2019-03-30 13:22:01 -07:00
JonnyWong16
b65a30263e Fix user IP address last seen 2019-03-30 12:10:30 -07:00
JonnyWong16
766e33df0e Add getPlexHeaders function 2019-03-26 09:10:09 -07:00
JonnyWong16
68df0f07c8 Return API result error when unauthenticaed 2019-03-21 10:22:34 -07:00
JonnyWong16
819829554b Use global port and root when getting self URL 2019-03-21 08:50:28 -07:00
JonnyWong16
18a38b16b1 Fix a9169d2 2019-03-20 08:58:30 -07:00
JonnyWong16
a9169d2b53 Fix terminate stream when both session_key and session_id are provided 2019-03-20 08:49:15 -07:00
JonnyWong16
76b9b3e474 Improve terminate stream error messages 2019-03-18 11:57:08 -07:00
JonnyWong16
00405f0b18 Change wording in activity header 2019-03-18 10:42:02 -07:00
JonnyWong16
9dfeccdaed Fix update metadata serach only returning 3 results 2019-03-17 18:19:11 -07:00
JonnyWong16
b6d044fe8f Fix API search not using the limit parameter 2019-03-17 17:59:18 -07:00
JonnyWong16
e949b1486e v2.1.28 2019-03-10 14:47:26 -07:00
JonnyWong16
8e1b6efc51 Reword unable to delete from Cloudinary log message 2019-03-10 14:32:45 -07:00
JonnyWong16
00012ffe09 Merge pull request #1344 from Jabcob/patch-1
Update init.systemd
2019-03-10 14:28:01 -07:00
Jabcob
bcf6b4de77 Update init.systemd
Edited user creation and directory ownership instructions in the configuration notes for clarity. Current version may give novice users the impression that the ownership command is only executed on CentOS/Fedora.
2019-03-09 15:39:26 -06:00
JonnyWong16
b1516e9963 Improve mass delete from Cloudinary 2019-03-08 17:49:28 -08:00
JonnyWong16
231de3a7a5 Merge pull request #1343 from samwiseg0/fix/relayed_int
Fix relayed to be an integer vs string
2019-03-06 18:33:05 -08:00
samwiseg0
b611ea659e Fix relayed to be an integer vs string 2019-03-06 17:02:12 -08:00
JonnyWong16
6e9f299c19 Add secure/insecure icon to activity card 2019-03-05 21:55:13 -08:00
JonnyWong16
61fac10079 v2.1.27-beta 2019-03-03 15:30:42 -08:00
JonnyWong16
536e8add17 Encode email exception to unicode 2019-03-03 14:10:05 -08:00
JonnyWong16
cb81bcac57 Return default blank content type (Fixes Tautulli/Tautulli-Issues#165) 2019-03-03 13:39:45 -08:00
JonnyWong16
5dd7806c0e Combine recently added manually due to change in Plex recently added API 2019-03-02 18:42:43 -08:00
JonnyWong16
2a707fc512 Fix typo in f6f5df3 2019-02-24 14:39:07 -08:00
JonnyWong16
469e54a22c Add current release/version to update_check API 2019-02-24 14:36:21 -08:00
JonnyWong16
f6f5df3d1e Add ability to dismiss browser warning 2019-02-24 14:27:34 -08:00
JonnyWong16
ae0960d2e2 Merge pull request #1341 from Arcanemagus/systemd-restart-note
Add Systemd auto restart policy
2019-02-24 14:00:30 -08:00
Landon Abney
a646cc36a1 Enable by default
With the protections from an infinite restart loop in place this should 
be safe to enable by default.
2019-02-24 13:58:19 -08:00
Landon Abney
b243ac5f5c Add burst failure protection
If the process fails 3 times within 90 seconds of a start attempt 
consider it permanently failed and stop all further attempts to restart 
it automatically.
2019-02-24 13:55:33 -08:00
JonnyWong16
bca7744bc5 Remove unicode from Email notification failed error message 2019-02-24 12:51:27 -08:00
JonnyWong16
2fc826c88f Fix some local variable references (Also Fixes Tautulli/Tautulli-Issues#155) 2019-02-24 12:34:55 -08:00
JonnyWong16
6397b1e5a7 Show tags in notification text preview modal 2019-02-24 11:18:18 -08:00
Landon Abney
85b9a47a0d Add note on how to restart automatically
If Tautulli ever crashes due to a failure of some sort the policies 
mentioned here will automatically restart it, with the caveat that they 
will _always_ restart it, even if it is going to crash right away again!
2019-02-22 23:09:34 -08:00
JonnyWong16
5749ab7c92 Remove execute permision from systemd init script (Fixes Tautulli/Tautulli-Issues#149) 2019-02-22 19:50:06 -08:00
JonnyWong16
dcb56cfd20 Add message to complete setup wizard 2019-02-20 21:38:13 -08:00
JonnyWong16
90849f9196 Remove crypto donation 2019-02-20 20:56:50 -08:00
JonnyWong16
5b77cab575 Update Patreon URL 2019-02-20 20:55:59 -08:00
JonnyWong16
6a21d7690a Improve data sanitation (Fixes Tautulli/Tautulli-Issues#161) 2019-02-20 18:35:04 -08:00
JonnyWong16
037e983350 Change PMS beta update check URL 2019-02-19 21:20:34 -08:00
Anthony Biacco
7f0cdb6f26 add config variable PMS_UPDATE_NOTIFY_INTERVAL and settings interface to be to change how often we notify about a new plex version 2018-12-12 10:18:11 -07:00
JonnyWong16
aa023f0166 v2.1.26 2018-12-01 15:50:04 -08:00
JonnyWong16
571b5461c0 Fix stream info graph modal history table (Fixes Tautulli/Tautulli-Issues#142) 2018-12-01 15:30:16 -08:00
JonnyWong16
a749b71f7f Fix activity resume after buffering 2018-12-01 15:17:33 -08:00
JonnyWong16
ac259214f7 Merge pull request #1333 from samwiseg00/add/user_email
Add user_email parameter for notifications
2018-11-04 11:49:42 -08:00
JonnyWong16
e11803685c Fix API error when missing cmd 2018-11-04 11:49:13 -08:00
samwiseg00
e4c3601312 Add user_email parameter for notifications 2018-11-04 14:18:34 -05:00
JonnyWong16
56a91de2c4 v2.1.25 2018-11-03 16:23:21 -07:00
JonnyWong16
e2d217a981 Merge pull request #1332 from samwiseg00/add/livetv_image
Add live TV images for current activity
2018-11-03 12:12:10 -07:00
samwiseg00
b484f27724 Add logic for live tv in current activity 2018-11-03 14:56:53 -04:00
samwiseg00
eb04a2e579 Fix poster image failback 2018-11-03 14:55:56 -04:00
samwiseg00
c66d8ecd5f Add new images for live tv activity 2018-11-03 14:55:30 -04:00
JonnyWong16
79b5f3c36f Merge pull request #1331 from samwiseg00/fix/pms_video_codec
Override * in video codecs
2018-11-02 19:59:48 -07:00
samwiseg00
4a78424b75 Override * in video codecs 2018-11-01 22:38:15 -04:00
JonnyWong16
4f78d0c98a Missing " in settings alert error 2018-10-31 17:49:33 -07:00
JonnyWong16
91b84b4437 Placeholder "-" for burn subtitle codec 2018-10-29 20:47:02 -07:00
JonnyWong16
8a9b3dc782 Override * in audio codecs 2018-10-29 20:46:37 -07:00
JonnyWong16
944df6cec1 v2.1.24-beta 2018-10-29 18:48:05 -07:00
JonnyWong16
354d46e940 Merge pull request #1330 from Arcanemagus/pytz-2018.6
Update pytz to 2018.6
2018-10-24 17:10:22 -04:00
Landon Abney
71cb2d9c4c Update pytz to 2018.6
Update the pytz library files to those from the 2018.6 release.
2018-10-24 12:48:53 -07:00
JonnyWong16
c08cec40cb Log colon 2018-10-23 22:45:28 -07:00
JonnyWong16
dee544c951 Update tzlocal to v1.5.1 2018-10-23 22:40:49 -07:00
JonnyWong16
f42581a5a6 Merge pull request #1329 from samwiseg00/add/timezone_log
Determine system timezone, log the timezone, and display it in the web ui config table
2018-10-23 22:05:06 -07:00
samwiseg00
fdc9f165a4 Add system timezone to the web configuration table 2018-10-23 21:55:32 -07:00
samwiseg00
8fff796700 Log system timezone on startup 2018-10-23 21:55:03 -07:00
samwiseg00
c64a115d29 Add logic to determine system timezone 2018-10-23 21:54:32 -07:00
JonnyWong16
d731ad851c Add queued tasks modal 2018-10-22 22:01:16 -07:00
JonnyWong16
c098175ba9 Merge pull request #1328 from samwiseg00/fix/on_change
Fix transcode change creating invalid sessions in the DB
2018-10-19 19:48:41 -07:00
JonnyWong16
d728f7a11e Version graphs to bypass cache 2018-10-19 18:58:26 -07:00
samwiseg00
7271e66ab8 Fix transcode change creating invalid sessions in the DB 2018-10-18 22:19:36 -04:00
JonnyWong16
5c54283df7 Merge pull request #1326 from samwiseg00/update/telegram_limit
Update character limit for telegram to reflect API changes.
2018-10-16 18:00:28 -07:00
samwiseg00
86b1d0f51d Update character limit for telegram to reflect API changes. 2018-10-16 16:23:56 -04:00
JonnyWong16
f2fedb182c Make local storage unique to Tautulli instance 2018-10-15 18:20:16 -07:00
1409 changed files with 135091 additions and 41150 deletions

10
.dockerignore Normal file
View File

@@ -0,0 +1,10 @@
.git
.github
.gitignore
contrib
init-scripts
package
pylintrc
*.md
!CHANGELOG*.md
start.bat

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
github: JonnyWong16
patreon: Tautulli
custom: ["https://bit.ly/2InPp15"]

95
.github/workflows/publish-docker.yml vendored Normal file
View File

@@ -0,0 +1,95 @@
name: Publish Docker
on:
push:
branches: [master, beta, nightly, python3]
tags: [v*]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Prepare
id: prepare
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo ::set-output name=tag::${GITHUB_REF#refs/tags/}
elif [[ $GITHUB_REF == refs/heads/master ]]; then
echo ::set-output name=tag::latest
else
echo ::set-output name=tag::${GITHUB_REF#refs/heads/}
fi
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo ::set-output name=branch::master
else
echo ::set-output name=branch::${GITHUB_REF#refs/heads/}
fi
echo ::set-output name=commit::${GITHUB_SHA}
echo ::set-output name=build_date::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
echo ::set-output name=docker_platforms::linux/amd64,linux/arm64,linux/arm
echo ::set-output name=docker_image::${{ secrets.DOCKER_REPO }}/tautulli
- name: Set up Docker Buildx
id: buildx
uses: crazy-max/ghaction-docker-buildx@v3
with:
buildx-version: latest
- name: Cache Docker Layers
id: cache
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-
- name: Docker Buildx (no push)
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--cache-to "type=local,dest=/tmp/.buildx-cache" \
--platform ${{ steps.prepare.outputs.docker_platforms }} \
--output "type=image,push=false" \
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
--file Dockerfile .
- name: Docker Login
if: success()
env:
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
run: |
echo "${DOCKER_PASSWORD}" | docker login --username "${{ secrets.DOCKER_USERNAME }}" --password-stdin
- name: Docker Buildx (push)
if: success()
run: |
docker buildx build \
--cache-from "type=local,src=/tmp/.buildx-cache" \
--platform ${{ steps.prepare.outputs.docker_platforms }} \
--output "type=image,push=true" \
--build-arg "TAG=${{ steps.prepare.outputs.tag }}" \
--build-arg "BRANCH=${{ steps.prepare.outputs.branch }}" \
--build-arg "COMMIT=${{ steps.prepare.outputs.commit }}" \
--build-arg "BUILD_DATE=${{ steps.prepare.outputs.build_date }}" \
--tag "${{ steps.prepare.outputs.docker_image }}:${{ steps.prepare.outputs.tag }}" \
--file Dockerfile .
- name: Clear
if: always()
run: |
rm -f ${HOME}/.docker/config.json
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: ${{ github.workflow }}
nofail: true

204
.github/workflows/publish-release.yml vendored Normal file
View File

@@ -0,0 +1,204 @@
name: Publish Release
on:
push:
branches: [master, beta, nightly, python3]
tags: [v*]
jobs:
build-windows:
runs-on: windows-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set Release Version
id: get_version
shell: bash
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
VERSION_NSIS=${GITHUB_REF#refs/tags/v}.1
echo ::set-output name=VERSION_NSIS::${VERSION_NSIS/%-beta.1/.0}
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
else
echo ::set-output name=VERSION_NSIS::0.0.0.0
echo ::set-output name=VERSION::0.0.0
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
fi
echo $GITHUB_SHA > version.txt
- name: Set Up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Cache Dependencies
id: cache_dependencies
uses: actions/cache@v2
with:
path: ~\AppData\Local\pip\Cache
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-windows.txt') }}
restore-keys: ${{ runner.os }}-pip-
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r package/requirements-windows.txt
- name: Build Package
run: |
pyinstaller -y ./package/Tautulli-windows.spec
- name: Create Installer
uses: joncloud/makensis-action@v1.2
with:
script-file: ./package/Tautulli.nsi
arguments: /DVERSION=${{ steps.get_version.outputs.VERSION_NSIS }} /DINSTALLER_NAME=..\Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
include-more-plugins: true
include-custom-plugins-path: package/nsis-plugins
- name: Upload Installer
uses: actions/upload-artifact@v2
with:
name: Tautulli-windows-installer
path: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: Build Windows Installer
nofail: true
build-macos:
runs-on: macos-latest
steps:
- name: Checkout Code
uses: actions/checkout@v2
- name: Set Release Version
id: get_version
shell: bash
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
echo ::set-env name=VERSION::${GITHUB_REF#refs/tags/v}
echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/v}
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
else
echo ::set-env name=VERSION::0.0.0
echo ::set-output name=VERSION::0.0.0
echo ::set-output name=RELEASE_VERSION::${GITHUB_SHA::7}
fi
echo $GITHUB_SHA > version.txt
- name: Set Up Python
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Cache Dependencies
id: cache_dependencies
uses: actions/cache@v2
with:
path: ~/Library/Caches/pip
key: ${{ runner.os }}-pip-${{ hashFiles('package/requirements-macos.txt') }}
restore-keys: ${{ runner.os }}-pip-
- name: Install Dependencies
run: |
python -m pip install --upgrade pip
pip install -r package/requirements-macos.txt
- name: Build Package
run: |
pyinstaller -y ./package/Tautulli-macos.spec
- name: Create Installer
run: |
sudo pkgbuild --install-location /Applications --version ${{ steps.get_version.outputs.VERSION }} --component ./dist/Tautulli.app --scripts ./package/macos-scripts Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
- name: Upload Installer
uses: actions/upload-artifact@v2
with:
name: Tautulli-macos-installer
path: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
title: Build MacOS Installer
nofail: true
release:
needs: [build-windows, build-macos]
if: startsWith(github.ref, 'refs/tags/') && always()
runs-on: ubuntu-latest
steps:
- name: Get Build Job Status
uses: technote-space/workflow-conclusion-action@v1
- name: Checkout Code
uses: actions/checkout@v2
- name: Set Release Version
id: get_version
run: |
echo ::set-output name=RELEASE_VERSION::${GITHUB_REF#refs/tags/}
- name: Download Windows Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/download-artifact@v2
with:
name: Tautulli-windows-installer
- name: Download MacOS Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/download-artifact@v2
with:
name: Tautulli-macos-installer
- name: Get Changelog
id: get_changelog
run: echo ::set-output name=CHANGELOG::"$( sed -n '/^## /{p; :loop n; p; /^## /q; b loop}' CHANGELOG.md | sed '$d' | sed '$d' | sed '$d' | sed ':a;N;$!ba;s/\n/%0A/g' )"
- name: Create Release
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.get_version.outputs.RELEASE_VERSION }}
release_name: Tautulli ${{ steps.get_version.outputs.RELEASE_VERSION }}
body: |
## Changelog
##${{ steps.get_changelog.outputs.CHANGELOG }}
draft: false
prerelease: ${{ endsWith(steps.get_version.outputs.RELEASE_VERSION, '-beta') }}
- name: Upload Windows Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
asset_name: Tautulli-windows-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.exe
asset_content_type: application/vnd.microsoft.portable-executable
- name: Upload MacOS Installer
if: env.WORKFLOW_CONCLUSION == 'success'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
asset_name: Tautulli-macos-${{ steps.get_version.outputs.RELEASE_VERSION }}-x64.pkg
asset_content_type: application/vnd.apple.installer+xml

6
.gitignore vendored
View File

@@ -19,6 +19,8 @@ backups/*
cache/* cache/*
newsletters/* newsletters/*
*.mmdb *.mmdb
version.txt
branch.txt
# HTTPS Cert/Key # # HTTPS Cert/Key #
################## ##################
@@ -74,3 +76,7 @@ _ReSharper*/
/logs /logs
.project .project
.pydevproject .pydevproject
#Ignore files generated by pyinstaller
/build
/dist

332
API.md
View File

@@ -85,10 +85,11 @@ Delete all Tautulli history for a specific library.
``` ```
Required parameters: Required parameters:
server_id (str): The Plex server identifier of the library section
section_id (str): The id of the Plex library section section_id (str): The id of the Plex library section
Optional parameters: Optional parameters:
None row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns: Returns:
None None
@@ -103,7 +104,7 @@ Required parameters:
user_id (str): The id of the Plex user user_id (str): The id of the Plex user
Optional parameters: Optional parameters:
None row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns: Returns:
None None
@@ -114,6 +115,21 @@ Returns:
Delete and recreate the cache directory. Delete and recreate the cache directory.
### delete_history
Delete history rows from Tautulli.
```
Required parameters:
row_ids (str): Comma separated row ids to delete, e.g. "65,110,2,3645"
Optional parameters:
None
Returns:
None
```
### delete_hosted_images ### delete_hosted_images
Delete the images uploaded to image hosting services. Delete the images uploaded to image hosting services.
@@ -143,10 +159,11 @@ Delete a library section from Tautulli. Also erases all history for the library.
``` ```
Required parameters: Required parameters:
server_id (str): The Plex server identifier of the library section
section_id (str): The id of the Plex library section section_id (str): The id of the Plex library section
Optional parameters: Optional parameters:
None row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns: Returns:
None None
@@ -173,11 +190,14 @@ Delete the 3rd party API lookup info.
``` ```
Required parameters: Required parameters:
rating_key (int): 1234
(Note: Must be the movie, show, or artist rating key)
Optional parameters:
None None
Optional parameters:
rating_key (int): 1234
(Note: Must be the movie, show, artist, album, or track rating key)
service (str): 'themoviedb' or 'tvmaze' or 'musicbrainz'
delete_all (bool): 'true' to delete all images form the service
Returns: Returns:
json: json:
{"result": "success", {"result": "success",
@@ -275,6 +295,10 @@ Returns:
``` ```
### delete_recently_added
Flush out all of the recently added items in the database.
### delete_temp_sessions ### delete_temp_sessions
Flush out all of the temporary sessions in the database. Flush out all of the temporary sessions in the database.
@@ -287,7 +311,7 @@ Required parameters:
user_id (str): The id of the Plex user user_id (str): The id of the Plex user
Optional parameters: Optional parameters:
None row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns: Returns:
None None
@@ -327,6 +351,7 @@ Required parameters:
Optional parameters: Optional parameters:
custom_thumb (str): The URL for the custom library thumbnail custom_thumb (str): The URL for the custom library thumbnail
custom_art (str): The URL for the custom library background art
keep_history (int): 0 or 1 keep_history (int): 0 or 1
Returns: Returns:
@@ -395,9 +420,14 @@ Returns:
"banner": "/library/metadata/1219/banner/1503306930", "banner": "/library/metadata/1219/banner/1503306930",
"bif_thumb": "/library/parts/274169/indexes/sd/1000", "bif_thumb": "/library/parts/274169/indexes/sd/1000",
"bitrate": "10617", "bitrate": "10617",
"channel_call_sign": "",
"channel_identifier": "",
"channel_stream": 0, "channel_stream": 0,
"channel_thumb": "",
"children_count": "",
"collections": [], "collections": [],
"container": "mkv", "container": "mkv",
"container_decision": "direct play",
"content_rating": "TV-MA", "content_rating": "TV-MA",
"deleted_user": 0, "deleted_user": 0,
"device": "Windows", "device": "Windows",
@@ -416,6 +446,7 @@ Returns:
"Drama", "Drama",
"Fantasy" "Fantasy"
], ],
"grandparent_guid": "com.plexapp.agents.thetvdb://121361?lang=en",
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1503306930", "grandparent_thumb": "/library/metadata/1219/thumb/1503306930",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
@@ -426,13 +457,15 @@ Returns:
"ip_address": "10.10.10.1", "ip_address": "10.10.10.1",
"ip_address_public": "64.123.23.111", "ip_address_public": "64.123.23.111",
"is_admin": 1, "is_admin": 1,
"is_allow_sync": null, "is_allow_sync": 1,
"is_home_user": 1, "is_home_user": 1,
"is_restricted": 0, "is_restricted": 0,
"keep_history": 1, "keep_history": 1,
"labels": [], "labels": [],
"last_viewed_at": "1462165717", "last_viewed_at": "1462165717",
"library_name": "TV Shows", "library_name": "TV Shows",
"live": 0,
"live_uuid": "",
"local": "1", "local": "1",
"location": "lan", "location": "lan",
"machine_id": "lmd93nkn12k29j2lnm", "machine_id": "lmd93nkn12k29j2lnm",
@@ -441,8 +474,9 @@ Returns:
"optimized_version": 0, "optimized_version": 0,
"optimized_version_profile": "", "optimized_version_profile": "",
"optimized_version_title": "", "optimized_version_title": "",
"originally_available_at": "2016-04-24",
"original_title": "", "original_title": "",
"originally_available_at": "2016-04-24",
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1503889210", "parent_thumb": "/library/metadata/153036/thumb/1503889210",
@@ -461,6 +495,7 @@ Returns:
"rating_key": "153037", "rating_key": "153037",
"relay": 0, "relay": 0,
"section_id": "2", "section_id": "2",
"secure": 1,
"session_id": "helf15l3rxgw01xxe0jf3l3d", "session_id": "helf15l3rxgw01xxe0jf3l3d",
"session_key": "27", "session_key": "27",
"shared_libraries": [ "shared_libraries": [
@@ -499,15 +534,23 @@ Returns:
"stream_subtitle_location": "", "stream_subtitle_location": "",
"stream_video_bit_depth": "8", "stream_video_bit_depth": "8",
"stream_video_bitrate": "10233", "stream_video_bitrate": "10233",
"stream_video_chroma_subsampling": "4:2:0",
"stream_video_codec": "h264", "stream_video_codec": "h264",
"stream_video_codec_level": "41", "stream_video_codec_level": "41",
"stream_video_color_primaries": "",
"stream_video_color_range": "tv",
"stream_video_color_space": "bt709",
"stream_video_color_trc": "",
"stream_video_decision": "direct play", "stream_video_decision": "direct play",
"stream_video_dynamic_range": "SDR",
"stream_video_framerate": "24p", "stream_video_framerate": "24p",
"stream_video_full_resolution": "1080p",
"stream_video_height": "1078", "stream_video_height": "1078",
"stream_video_language": "", "stream_video_language": "",
"stream_video_language_code": "", "stream_video_language_code": "",
"stream_video_ref_frames": "4", "stream_video_ref_frames": "4",
"stream_video_resolution": "1080", "stream_video_resolution": "1080",
"stream_video_scan_type": "progressive",
"stream_video_width": "1920", "stream_video_width": "1920",
"studio": "HBO", "studio": "HBO",
"subtitle_codec": "", "subtitle_codec": "",
@@ -555,17 +598,25 @@ Returns:
"username": "LordCommanderSnow", "username": "LordCommanderSnow",
"video_bit_depth": "8", "video_bit_depth": "8",
"video_bitrate": "10233", "video_bitrate": "10233",
"video_chroma_subsampling": "4:2:0",
"video_codec": "h264", "video_codec": "h264",
"video_codec_level": "41", "video_codec_level": "41",
"video_color_primaries": "",
"video_color_range": "tv",
"video_color_space": "bt709",
"video_color_trc": ",
"video_decision": "direct play", "video_decision": "direct play",
"video_dynamic_range": "SDR",
"video_frame_rate": "23.976", "video_frame_rate": "23.976",
"video_framerate": "24p", "video_framerate": "24p",
"video_full_resolution": "1080p",
"video_height": "1078", "video_height": "1078",
"video_language": "", "video_language": "",
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_resolution": "1080", "video_resolution": "1080",
"video_scan_type": "progressive",
"video_width": "1920", "video_width": "1920",
"view_offset": "1000", "view_offset": "1000",
"width": "1920", "width": "1920",
@@ -622,7 +673,7 @@ Returns:
### get_geoip_lookup ### get_geoip_lookup
Get the geolocation info for an IP address. The GeoLite2 database must be installed. Get the geolocation info for an IP address.
``` ```
Required parameters: Required parameters:
@@ -633,7 +684,7 @@ Optional parameters:
Returns: Returns:
json: json:
{"continent": "North America", {"code": 'US",
"country": "United States", "country": "United States",
"region": "California", "region": "California",
"city": "Mountain View", "city": "Mountain View",
@@ -643,9 +694,6 @@ Returns:
"longitude": -122.0838, "longitude": -122.0838,
"accuracy": 1000 "accuracy": 1000
} }
json:
{"error": "The address 127.0.0.1 is not in the database."
}
``` ```
@@ -665,8 +713,9 @@ Optional parameters:
grandparent_rating_key (int): 351 grandparent_rating_key (int): 351
start_date (str): "YYYY-MM-DD" start_date (str): "YYYY-MM-DD"
section_id (int): 2 section_id (int): 2
media_type (str): "movie", "episode", "track" media_type (str): "movie", "episode", "track", "live"
transcode_decision (str): "direct play", "copy", "transcode", transcode_decision (str): "direct play", "copy", "transcode",
guid (str): Plex guid for an item, e.g. "com.plexapp.agents.thetvdb://121361/6/1"
order_column (str): "date", "friendly_name", "ip_address", "platform", "player", order_column (str): "date", "friendly_name", "ip_address", "platform", "player",
"full_title", "started", "paused_counter", "stopped", "duration" "full_title", "started", "paused_counter", "stopped", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
@@ -691,19 +740,23 @@ Returns:
"original_title": "", "original_title": "",
"group_count": 1, "group_count": 1,
"group_ids": "1124", "group_ids": "1124",
"id": 1124, "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"ip_address": "xxx.xxx.xxx.xxx", "ip_address": "xxx.xxx.xxx.xxx",
"live": 0,
"media_index": 17, "media_index": 17,
"media_type": "episode", "media_type": "episode",
"originally_available_at": "2016-04-24",
"parent_media_index": 7, "parent_media_index": 7,
"parent_rating_key": 544, "parent_rating_key": 544,
"parent_title": "", "parent_title": "",
"paused_counter": 0, "paused_counter": 0,
"percent_complete": 84, "percent_complete": 84,
"platform": "Chrome", "platform": "Windows",
"player": "Plex Web (Chrome)", "product": "Plex for Windows",
"player": "Castle-PC",
"rating_key": 4348, "rating_key": 4348,
"reference_id": 1123, "reference_id": 1123,
"row_id": 1124,
"session_key": null, "session_key": null,
"started": 1462688107, "started": 1462688107,
"state": null, "state": null,
@@ -751,8 +804,10 @@ Returns:
[{"content_rating": "TV-MA", [{"content_rating": "TV-MA",
"friendly_name": "", "friendly_name": "",
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"labels": [], "labels": [],
"last_play": 1462380698, "last_play": 1462380698,
"live": 0,
"media_type": "episode", "media_type": "episode",
"platform": "", "platform": "",
"platform_type": "", "platform_type": "",
@@ -813,6 +868,7 @@ Returns:
[{"art": "/:/resources/show-fanart.jpg", [{"art": "/:/resources/show-fanart.jpg",
"child_count": "3745", "child_count": "3745",
"count": "62", "count": "62",
"is_active": 1,
"parent_count": "240", "parent_count": "240",
"section_id": "2", "section_id": "2",
"section_name": "TV Shows", "section_name": "TV Shows",
@@ -833,6 +889,7 @@ Required parameters:
None None
Optional parameters: Optional parameters:
grouping (int): 0 or 1
order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count", order_column (str): "library_thumb", "section_name", "section_type", "count", "parent_count",
"child_count", "last_accessed", "last_played", "plays", "duration" "child_count", "last_accessed", "last_played", "plays", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
@@ -852,23 +909,29 @@ Returns:
"do_notify": "Checked", "do_notify": "Checked",
"do_notify_created": "Checked", "do_notify_created": "Checked",
"duration": 1578037, "duration": 1578037,
"id": 1128, "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"histroy_row_id": 1128,
"is_active": 1,
"keep_history": "Checked", "keep_history": "Checked",
"labels": [], "labels": [],
"last_accessed": 1462693216, "last_accessed": 1462693216,
"last_played": "Game of Thrones - The Red Woman", "last_played": "Game of Thrones - The Red Woman",
"library_art": "/:/resources/show-fanart.jpg", "library_art": "/:/resources/show-fanart.jpg",
"library_thumb": "", "library_thumb": "/:/resources/show.png",
"live": 0,
"media_index": 1, "media_index": 1,
"media_type": "episode", "media_type": "episode",
"originally_available_at": "2016-04-24",
"parent_count": 240, "parent_count": 240,
"parent_media_index": 6, "parent_media_index": 6,
"parent_title": "", "parent_title": "",
"plays": 772, "plays": 772,
"rating_key": 153037, "rating_key": 153037,
"row_id": 1,
"section_id": 2, "section_id": 2,
"section_name": "TV Shows", "section_name": "TV Shows",
"section_type": "Show", "section_type": "Show",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w",
"thumb": "/library/metadata/153036/thumb/1462175062", "thumb": "/library/metadata/153036/thumb/1462175062",
"year": 2016 "year": 2016
}, },
@@ -893,15 +956,19 @@ Returns:
json: json:
{"child_count": null, {"child_count": null,
"count": 887, "count": 887,
"deleted_section": 0,
"do_notify": 1, "do_notify": 1,
"do_notify_created": 1, "do_notify_created": 1,
"is_active": 1,
"keep_history": 1, "keep_history": 1,
"library_art": "/:/resources/movie-fanart.jpg", "library_art": "/:/resources/movie-fanart.jpg",
"library_thumb": "/:/resources/movie.png", "library_thumb": "/:/resources/movie.png",
"parent_count": null, "parent_count": null,
"row_id": 1,
"section_id": 1, "section_id": 1,
"section_name": "Movies", "section_name": "Movies",
"section_type": "movie" "section_type": "movie",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
} }
``` ```
@@ -949,6 +1016,7 @@ Returns:
"rating_key": "1219", "rating_key": "1219",
"section_id": 2, "section_id": 2,
"section_type": "show", "section_type": "show",
"sort_title": "Game of Thrones",
"thumb": "/library/metadata/1219/thumb/1436265995", "thumb": "/library/metadata/1219/thumb/1436265995",
"title": "Game of Thrones", "title": "Game of Thrones",
"video_codec": "", "video_codec": "",
@@ -998,12 +1066,14 @@ Returns:
[{"friendly_name": "Jon Snow", [{"friendly_name": "Jon Snow",
"total_plays": 170, "total_plays": 170,
"user_id": 133788, "user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar" "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
"username": "LordCommanderSnow"
}, },
{"platform_type": "DanyKhaleesi69", {"platform_type": "DanyKhaleesi69",
"total_plays": 42, "total_plays": 42,
"user_id": 8008135, "user_id": 8008135,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar" "user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar",
"username: "DanyKhaleesi69"
}, },
{...}, {...},
{...} {...}
@@ -1016,10 +1086,11 @@ Get a library's watch time statistics.
``` ```
Required parameters: Required parameters:
section_id (str): The id of the Plex library section section_id (str): The id of the Plex library section
Optional parameters: Optional parameters:
grouping (int): 0 or 1 grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns: Returns:
json: json:
@@ -1107,13 +1178,16 @@ Returns:
"Drama", "Drama",
"Fantasy" "Fantasy"
], ],
"grandparent_guid": "com.plexapp.agents.thetvdb://121361?lang=en",
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en", "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"guids": [],
"labels": [], "labels": [],
"last_viewed_at": "1462165717", "last_viewed_at": "1462165717",
"library_name": "TV Shows", "library_name": "TV Shows",
"live": 0,
"media_index": "1", "media_index": "1",
"media_info": [ "media_info": [
{ {
@@ -1123,6 +1197,9 @@ Returns:
"audio_codec": "ac3", "audio_codec": "ac3",
"audio_profile": "", "audio_profile": "",
"bitrate": "10617", "bitrate": "10617",
"channel_call_sign": "",
"channel_identifier": "",
"channel_thumb": "",
"container": "mkv", "container": "mkv",
"height": "1078", "height": "1078",
"id": "257925", "id": "257925",
@@ -1141,12 +1218,17 @@ Returns:
"video_bitrate": "10233", "video_bitrate": "10233",
"video_codec": "h264", "video_codec": "h264",
"video_codec_level": "41", "video_codec_level": "41",
"video_color_primaries": "",
"video_color_range": "tv",
"video_color_space": "bt709",
"video_color_trc": "",
"video_frame_rate": "23.976", "video_frame_rate": "23.976",
"video_height": "1078", "video_height": "1078",
"video_language": "", "video_language": "",
"video_language_code": "", "video_language_code": "",
"video_profile": "high", "video_profile": "high",
"video_ref_frames": "4", "video_ref_frames": "4",
"video_scan_type": "progressive",
"video_width": "1920", "video_width": "1920",
"selected": 0 "selected": 0
}, },
@@ -1181,6 +1263,7 @@ Returns:
], ],
"video_codec": "h264", "video_codec": "h264",
"video_framerate": "24p", "video_framerate": "24p",
"video_full_resolution": "1080p",
"video_profile": "high", "video_profile": "high",
"video_resolution": "1080", "video_resolution": "1080",
"width": "1920" "width": "1920"
@@ -1189,6 +1272,7 @@ Returns:
"media_type": "episode", "media_type": "episode",
"original_title": "", "original_title": "",
"originally_available_at": "2016-04-24", "originally_available_at": "2016-04-24",
"parent_guid": "com.plexapp.agents.thetvdb://121361/6?lang=en",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
@@ -1197,7 +1281,7 @@ Returns:
"rating_image": "rottentomatoes://image.rating.ripe", "rating_image": "rottentomatoes://image.rating.ripe",
"rating_key": "153037", "rating_key": "153037",
"section_id": "2", "section_id": "2",
"sort_title": "Game of Thrones", "sort_title": "Red Woman",
"studio": "HBO", "studio": "HBO",
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.", "summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
"tagline": "", "tagline": "",
@@ -1493,7 +1577,8 @@ Returns:
"series": "series":
[{"name": "Movies", "data": [...]} [{"name": "Movies", "data": [...]}
{"name": "TV", "data": [...]}, {"name": "TV", "data": [...]},
{"name": "Music", "data": [...]} {"name": "Music", "data": [...]},
{"name": "Live TV", "data": [...]}
] ]
} }
``` ```
@@ -1519,7 +1604,8 @@ Returns:
"series": "series":
[{"name": "Movies", "data": [...]} [{"name": "Movies", "data": [...]}
{"name": "TV", "data": [...]}, {"name": "TV", "data": [...]},
{"name": "Music", "data": [...]} {"name": "Music", "data": [...]},
{"name": "Live TV", "data": [...]}
] ]
} }
``` ```
@@ -1545,7 +1631,8 @@ Returns:
"series": "series":
[{"name": "Movies", "data": [...]} [{"name": "Movies", "data": [...]}
{"name": "TV", "data": [...]}, {"name": "TV", "data": [...]},
{"name": "Music", "data": [...]} {"name": "Music", "data": [...]},
{"name": "Live TV", "data": [...]}
] ]
} }
``` ```
@@ -1649,7 +1736,8 @@ Returns:
"series": "series":
[{"name": "Movies", "data": [...]} [{"name": "Movies", "data": [...]}
{"name": "TV", "data": [...]}, {"name": "TV", "data": [...]},
{"name": "Music", "data": [...]} {"name": "Music", "data": [...]},
{"name": "Live TV", "data": [...]}
] ]
} }
``` ```
@@ -1675,7 +1763,8 @@ Returns:
"series": "series":
[{"name": "Movies", "data": [...]} [{"name": "Movies", "data": [...]}
{"name": "TV", "data": [...]}, {"name": "TV", "data": [...]},
{"name": "Music", "data": [...]} {"name": "Music", "data": [...]},
{"name": "Live TV", "data": [...]}
] ]
} }
``` ```
@@ -1701,7 +1790,8 @@ Returns:
"series": "series":
[{"name": "Movies", "data": [...]} [{"name": "Movies", "data": [...]}
{"name": "TV", "data": [...]}, {"name": "TV", "data": [...]},
{"name": "Music", "data": [...]} {"name": "Music", "data": [...]},
{"name": "Live TV", "data": [...]}
] ]
} }
``` ```
@@ -1789,22 +1879,60 @@ Optional parameters:
Returns: Returns:
json: json:
{"recently_added": {"recently_added":
[{"added_at": "1461572396", [{"actors": [
"Kit Harington",
"Emilia Clarke",
"Isaac Hempstead-Wright",
"Maisie Williams",
"Liam Cunningham",
],
"added_at": "1461572396",
"art": "/library/metadata/1219/art/1462175063",
"audience_rating": "8",
"audience_rating_image": "rottentomatoes://image.rating.upright",
"banner": "/library/metadata/1219/banner/1462175063",
"directors": [
"Jeremy Podeswa"
],
"duration": "2998290",
"full_title": "Game of Thrones - The Red Woman",
"genres": [
"Adventure",
"Drama",
"Fantasy"
],
"grandparent_rating_key": "1219", "grandparent_rating_key": "1219",
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063", "grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones", "grandparent_title": "Game of Thrones",
"library_name": "", "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"guids": [],
"labels": [],
"last_viewed_at": "1462165717",
"library_name": "TV Shows",
"media_index": "1", "media_index": "1",
"media_type": "episode", "media_type": "episode",
"original_title": "", "original_title": "",
"originally_available_at": "2016-04-24",
"parent_media_index": "6", "parent_media_index": "6",
"parent_rating_key": "153036", "parent_rating_key": "153036",
"parent_thumb": "/library/metadata/153036/thumb/1462175062", "parent_thumb": "/library/metadata/153036/thumb/1462175062",
"parent_title": "", "parent_title": "",
"rating": "7.8",
"rating_image": "rottentomatoes://image.rating.ripe",
"rating_key": "153037", "rating_key": "153037",
"section_id": "2", "section_id": "2",
"sort_title": "Red Woman",
"studio": "HBO",
"summary": "Jon Snow is dead. Daenerys meets a strong man. Cersei sees her daughter again.",
"tagline": "",
"thumb": "/library/metadata/153037/thumb/1462175060", "thumb": "/library/metadata/153037/thumb/1462175060",
"title": "The Red Woman", "title": "The Red Woman",
"user_rating": "9.0",
"updated_at": "1462175060",
"writers": [
"David Benioff",
"D. B. Weiss"
],
"year": "2016" "year": "2016"
}, },
{...}, {...},
@@ -1866,6 +1994,33 @@ Returns:
``` ```
### get_server_info
Get the PMS server information.
```
Required parameters:
None
Optional parameters:
None
Returns:
json:
{"pms_identifier": "08u2phnlkdshf890bhdlksghnljsahgleikjfg9t",
"pms_ip": "10.10.10.1",
"pms_is_remote": 0,
"pms_name": "Winterfell-Server",
"pms_platform": "Windows",
"pms_plexpass": 1,
"pms_port": 32400,
"pms_ssl": 0,
"pms_url": "http://10.10.10.1:32400",
"pms_url_manual": 0,
"pms_version": "1.20.0.3133-fede5bdc7"
}
```
### get_server_list ### get_server_list
Get all your servers that are published to Plex.tv. Get all your servers that are published to Plex.tv.
@@ -1986,6 +2141,7 @@ Returns:
"stream_video_bitrate": 527, "stream_video_bitrate": 527,
"stream_video_codec": "h264", "stream_video_codec": "h264",
"stream_video_decision": "transcode", "stream_video_decision": "transcode",
"stream_video_dynamic_range": "SDR",
"stream_video_framerate": "24p", "stream_video_framerate": "24p",
"stream_video_height": 306, "stream_video_height": 306,
"stream_video_resolution": "SD", "stream_video_resolution": "SD",
@@ -2000,6 +2156,7 @@ Returns:
"video_bitrate": 2500, "video_bitrate": 2500,
"video_codec": "h264", "video_codec": "h264",
"video_decision": "transcode", "video_decision": "transcode",
"video_dynamic_range": "SDR",
"video_framerate": "24p", "video_framerate": "24p",
"video_height": 816, "video_height": 816,
"video_resolution": "1080", "video_resolution": "1080",
@@ -2119,10 +2276,13 @@ Returns:
"do_notify": 1, "do_notify": 1,
"email": "Jon.Snow.1337@CastleBlack.com", "email": "Jon.Snow.1337@CastleBlack.com",
"friendly_name": "Jon Snow", "friendly_name": "Jon Snow",
"is_active": 1,
"is_admin": 0,
"is_allow_sync": 1, "is_allow_sync": 1,
"is_home_user": 1, "is_home_user": 1,
"is_restricted": 0, "is_restricted": 0,
"keep_history": 1, "keep_history": 1,
"row_id": 1,
"shared_libraries": ["10", "1", "4", "5", "15", "20", "2"], "shared_libraries": ["10", "1", "4", "5", "15", "20", "2"],
"user_id": 133788, "user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", "user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
@@ -2139,8 +2299,8 @@ Required parameters:
user_id (str): The id of the Plex user user_id (str): The id of the Plex user
Optional parameters: Optional parameters:
order_column (str): "last_seen", "ip_address", "platform", "player", order_column (str): "last_seen", "first_seen", "ip_address", "platform",
"last_played", "play_count" "player", "last_played", "play_count"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
start (int): Row to start from, 0 start (int): Row to start from, 0
length (int): Number of items to return, 25 length (int): Number of items to return, 25
@@ -2153,12 +2313,16 @@ Returns:
"recordsFiltered": 10, "recordsFiltered": 10,
"data": "data":
[{"friendly_name": "Jon Snow", [{"friendly_name": "Jon Snow",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1121, "id": 1121,
"ip_address": "xxx.xxx.xxx.xxx", "ip_address": "xxx.xxx.xxx.xxx",
"last_played": "Game of Thrones - The Red Woman", "last_played": "Game of Thrones - The Red Woman",
"last_seen": 1462591869, "last_seen": 1462591869,
"first_seen": 1583968210,
"live": 0,
"media_index": 1, "media_index": 1,
"media_type": "episode", "media_type": "episode",
"originally_available_at": "2016-04-24",
"parent_media_index": 6, "parent_media_index": 6,
"parent_title": "", "parent_title": "",
"platform": "Chrome", "platform": "Chrome",
@@ -2272,6 +2436,7 @@ Required parameters:
Optional parameters: Optional parameters:
grouping (int): 0 or 1 grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns: Returns:
json: json:
@@ -2315,11 +2480,13 @@ Returns:
"filter_music": "", "filter_music": "",
"filter_photos": "", "filter_photos": "",
"filter_tv": "", "filter_tv": "",
"is_active": 1,
"is_admin": 0, "is_admin": 0,
"is_allow_sync": 1, "is_allow_sync": 1,
"is_home_user": 1, "is_home_user": 1,
"is_restricted": 0, "is_restricted": 0,
"keep_history": 1, "keep_history": 1,
"row_id": 1,
"server_token": "PU9cMuQZxJKFBtGqHk68", "server_token": "PU9cMuQZxJKFBtGqHk68",
"shared_libraries": "1;2;3", "shared_libraries": "1;2;3",
"thumb": "https://plex.tv/users/k10w42309cynaopq/avatar", "thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
@@ -2340,6 +2507,7 @@ Required parameters:
None None
Optional parameters: Optional parameters:
grouping (int): 0 or 1
order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform", order_column (str): "user_thumb", "friendly_name", "last_seen", "ip_address", "platform",
"player", "last_played", "plays", "duration" "player", "last_played", "plays", "duration"
order_dir (str): "desc" or "asc" order_dir (str): "desc" or "asc"
@@ -2357,23 +2525,29 @@ Returns:
"do_notify": "Checked", "do_notify": "Checked",
"duration": 2998290, "duration": 2998290,
"friendly_name": "Jon Snow", "friendly_name": "Jon Snow",
"id": 1121, "guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"history_row_id": 1121,
"ip_address": "xxx.xxx.xxx.xxx", "ip_address": "xxx.xxx.xxx.xxx",
"is_active": 1,
"keep_history": "Checked", "keep_history": "Checked",
"last_played": "Game of Thrones - The Red Woman", "last_played": "Game of Thrones - The Red Woman",
"last_seen": 1462591869, "last_seen": 1462591869,
"live": 0,
"media_index": 1, "media_index": 1,
"media_type": "episode", "media_type": "episode",
"originally_available_at": "2016-04-24",
"parent_media_index": 6, "parent_media_index": 6,
"parent_title": "", "parent_title": "",
"platform": "Chrome", "platform": "Chrome",
"player": "Plex Web (Chrome)", "player": "Plex Web (Chrome)",
"plays": 487, "plays": 487,
"rating_key": 153037, "rating_key": 153037,
"row_id": 1,
"thumb": "/library/metadata/153036/thumb/1462175062", "thumb": "/library/metadata/153036/thumb/1462175062",
"transcode_decision": "transcode", "transcode_decision": "transcode",
"user_id": 133788, "user_id": 133788,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar", "user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar",
"username": "LordCommanderSnow",
"year": 2016 "year": 2016
}, },
{...}, {...},
@@ -2414,25 +2588,54 @@ Returns:
``` ```
### import_database ### import_config
Import a PlexWatch or Plexivity database into Tautulli. Import a Tautulli config file.
``` ```
Required parameters: Required parameters:
app (str): "plexwatch" or "plexivity" config_file (file): The config file to import (multipart/form-data)
database_path (str): The full path to the plexwatch database file or
table_name (str): "processed" or "grouped" config_path (str): The full path to the config file to import
Optional parameters: Optional parameters:
import_ignore_interval (int): The minimum number of seconds for a stream to import backup (bool): true or false whether to backup
the current config before importing
Returns: Returns:
None json:
{"result": "success",
"message": "Config import has started. Check the logs to monitor any problems. "
"Tautulli will restart automatically."
}
``` ```
### install_geoip_db ### import_database
Downloads and installs the GeoLite2 database Import a Tautulli, PlexWatch, or Plexivity database into Tautulli.
```
Required parameters:
app (str): "tautulli" or "plexwatch" or "plexivity"
database_file (file): The database file to import (multipart/form-data)
or
database_path (str): The full path to the database file to import
method (str): For Tautulli only, "merge" or "overwrite"
table_name (str): For PlexWatch or Plexivity only, "processed" or "grouped"
Optional parameters:
backup (bool): For Tautulli only, true or false whether to backup
the current database before importing
import_ignore_interval (int): For PlexWatch or Plexivity only, the minimum number
of seconds for a stream to import
Returns:
json:
{"result": "success",
"message": "Database import has started. Check the logs to monitor any problems."
}
```
### notify ### notify
@@ -2445,6 +2648,7 @@ Required parameters:
body (str): The body of the message body (str): The body of the message
Optional parameters: Optional parameters:
headers (str): The JSON headers for webhook notifications
script_args (str): The arguments for script notifications script_args (str): The arguments for script notifications
Returns: Returns:
@@ -2501,10 +2705,10 @@ Optional parameters:
width (str): 300 width (str): 300
height (str): 450 height (str): 450
opacity (str): 25 opacity (str): 25
background (str): 282828 background (str): Hex color, e.g. 282828
blur (str): 3 blur (str): 3
img_format (str): png img_format (str): png
fallback (str): "poster", "cover", "art" fallback (str): "poster", "cover", "art", "poster-live", "art-live", "art-live-full"
refresh (bool): True or False whether to refresh the image cache refresh (bool): True or False whether to refresh the image cache
return_hash (bool): True or False to return the self-hosted image hash instead of the image return_hash (bool): True or False to return the self-hosted image hash instead of the image
@@ -2526,14 +2730,18 @@ Registers the Tautulli Android App for notifications.
``` ```
Required parameters: Required parameters:
device_name (str): The device name of the Tautulli Android App device_id (str): The unique device identifier for the mobile device
device_id (str): The OneSignal device id of the Tautulli Android App device_name (str): The device name of the mobile device
Optional parameters: Optional parameters:
friendly_name (str): A friendly name to identify the mobile device friendly_name (str): A friendly name to identify the mobile device
onesignal_id (str): The OneSignal id for the mobile device
Returns: Returns:
None json:
{"pms_name": "Winterfell-Server",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
}
``` ```
@@ -2630,7 +2838,7 @@ Returns:
### sql ### sql
Query the Tautulli database with raw SQL. Automatically makes a backup of Query the Tautulli database with raw SQL. Automatically makes a backup of
the database if the latest backup is older then 24h. `api_sql` must be the database if the latest backup is older then 24h. `api_sql` must be
manually enabled in the config file. manually enabled in the config file while Tautulli is shut down.
``` ```
Required parameters: Required parameters:
@@ -2644,6 +2852,24 @@ Returns:
``` ```
### status
Get the current status of Tautulli.
```
Required parameters:
None
Optional parameters:
check (str): database
Returns:
json:
{"result": "success",
"message": "Ok",
}
```
### terminate_session ### terminate_session
Stop a streaming session. Stop a streaming session.
@@ -2692,10 +2918,6 @@ Returns:
``` ```
### uninstall_geoip_db
Uninstalls the GeoLite2 database
### update ### update
Update Tautulli. Update Tautulli.

View File

@@ -1,5 +1,492 @@
# Changelog # Changelog
## v2.5.6 (2020-10-02)
* Activity:
* Change: Renamed container "Transcode" to "Converting" on activity cards.
* Notifications:
* New: Added a silent notification option for Telegram. (Thanks @JohnnyKing94)
* New: Added container_decision notification parameter.
* New: Added notification trigger for Playback Error.
* New: Added remote access down notification threshold setting.
* Newsletter:
* Change: Stop flooring newsletter start date.
* UI:
* Fix: Unable to purge history from the library edit modal.
* Fix: QR code not showing up for localhost address when trying to register a device.
* New: Added library name to the fix metadata modal.
* API:
* New: Added default thumb and art to the Live TV library.
* Other:
* Fix: Synced items not loading for guest access.
* New: Schedule some more automatic database optimizations.
* Change: Added automatic uninstall before installing to the Windows installer.
## v2.5.5 (2020-09-06)
* Activity:
* Fix: Filter out TV show background theme music sessions.
* Notifications:
* New: Check Plex external guids for notification metadata provider links.
* UI:
* Fix: Incorrect sorting for user/library recently played items.
* API:
* Fix: get_synced_items API command returning error with empty result.
* Fix: Download API commands not returning the file.
* Fix: get_logs API command encoding error.
* Fix: get_user_player_stats API command returning error instead of empty result.
* New: Added get_server_info API command.
* New: Added external guids to get_metadata API command.
* New: Added support for multi-column sorting for datatable API commands.
* Change: get_activity API command return thumbnail override for clips.
* Change: get_libraries_table API command return custom library artwork.
* Other:
* Fix: Tautulli failed to run with a stale pid file.
* New: Added scheduled task to optimize the Tautulli database.
* Change: Update plexapi to 3.6.0.
* Change: Update some libraries for Python 3 compatibility.
## v2.5.4 (2020-07-31)
* Monitoring:
* Change: Montitoring remote access changed to use websockets. Refer to Tautulli/Tautulli-Issues#251 for details.
* Notifications:
* Fix: Uploading images to Cloudinary failed for titles with non-ASCII characters on Python 2.
* New: Added plex_id notification parameter.
* Remove: Running .exe files directly using script notifications is no longer supported.
* Remove: php, perl, and ruby prefix overrides for script notifications is no longer supported.
* Change: Stricter checking of file extensions for script notifications.
* Change: Fallback to The Movie Database lookup using title and year.
* Change: Fallback to TVmaze lookup using title.
* UI:
* New: Added ability to import a previous Tautullli configuration file in the settings.
* New: Added a browse button for settings which require a folder or file input.
* New: Added first streamed column to user IP addresses table. (Thanks @dotsam)
* New: Added The Movie Database rating image to media page.
* Change: Different icon to represent direct stream in the history tables.
* API:
* New: Updated API docs for importing a database and configuration file.
## v2.5.3 (2020-07-10)
* History:
* Fix: Unable to delete more than 1000 history entries at the same time.
* Notifications:
* Change: Python script notifications to run using the same Python interpreter as Tautulli.
* Newsletters:
* Fix: Unable to view newsletters with special characters.
* Other:
* Fix: Tautulli failing to start after enabling HTTPS when installed using the Windows / macOS installers.
* Fix: Startup script not working on macOS.
* Fix: Unable to hide dock icon on macOS with the pkg install. Refer to the FAQ regarding the Python rocket dock icon.
* Change: Added path to Python interpreter in system startup (daemon) scripts.
* Change: Added Python version to Google analytics.
## v2.5.2 (2020-07-01)
* Announcements:
* Tautulli now supports Python 3!
* Python 2 is still supported for the time being, but it is recommended to upgrade to Python 3.
* Notifications:
* Fix: Error uploading images to Cloudinary on Python 2.
* Fix: Testing browser notifications alert not disappearing.
* Change: Default recently added notification delay set to 300 seconds.
* UI:
* Fix: MacOS menu bar icon causing Tautulli to fail to start.
* Fix: Unable to login to Tautulli on Python 2.
* New: Windows and MacOS setting to enable Tautulli to start automatically when you login.
* New: Added menu bar icon for MacOS.
* New: Ability to import a Tautulli database in the settings.
* New: Added Tautulli news area on the settings page.
* New: Added platform icon for LG devices.
* Remove: Ability to login to Tautulli using a Plex username and password has been removed. Login using a Plex.tv account is only supported via OAuth.
* Mobile App:
* Fix: Improved API security and validation when registering the Android app.
* Docker:
* Fix: Docker container not respecting the PUID and PGID environment variables.
* Other:
* Fix: Error creating self-signed certificates on Python 3.
* Fix: Tautulli login session cookie not set on the HTTP root path.
* New: Windows and MacOS app installers to install Tautulli without needing Python installed.
## v2.2.4 (2020-05-16)
* Monitoring:
* Fix: Show "None" as the subtitle source on the activity card for user selected subtitles.
* UI:
* Fix: Deleted libraries were showing up on the homepage library cards.
* Fix: Libraries could get stuck as inactive in the database in some instances.
* API:
* Fix: Incorrect title was being returned for the get_history API command.
* Other:
* Fix: Plex remote access check was not being rescheduled after changing the settings.
## v2.2.3 (2020-05-01)
* Notifications:
* Fix: Notification grouping by season/album and show/artist not enabled by default.
* Fix: The rating key notification parameter was being overwritten when 3rd party lookup was enabled.
* Fix: Missing artist value for Musicbrainz lookup in certain situations.
* New: Added notification trigger for Tautulli database corruption.
* New: Added TAUTULLI_PYTHON_VERSION to script notification environment variables.
* New: Added Plex Android / iOS App notification agent.
* New: Added bandwidth notification parameters.
* New: Added user thumb to notification parameters.
* New: Added initial stream notification parameter and threshold setting to determine if a stream is the first stream of a continuous streaming session.
* New: Added Plex remote access notification parameters.
* Change: The file size notification parameter is now reported in SI units. (Thanks @aaronldunlap)
* UI:
* Fix: Delete lookup info from the media info page failing.
* Fix: XBMC platform icon not being redirected to the Kodi platform icon.
* Fix: History table was not being refreshed after deleting entries.
* New: Added icon on the users table to indicate if the user is not on the Plex server.
* New: Added icon on the libraries table to indicate if the library is not on the Plex server.
* Change: Improved deleting libraries so libraries with the same section ID are not also deleted.
* Mobile App:
* Fix: Temporary device token was not being invalidated after cancelling device registration.
* API:
* Fix: Returning XML from the API failing due to unicode characters.
* Fix: Grouping parameter for various API commands not falling back to default setting.
* New: Added time_queries parameter to get_library_watch_time_stats and get_user_watch_time_stats API command. (Thanks @KaasKop97)
* New: Added an "is_active" return value to the get_user, get_users, get_library, and get_libraries API commands which indicates if the user or library is on the Plex server.
* New: Added delete_history API command.
* Change: Added optional parameter for row_ids for delete_library, delete_user, delete_all_library_history, and delete_all_user_history API commands.
* Other:
* Fix: Update failing on CentOS due to an older git version.
* Fix: Manifest file for creating a web app had incorrect info.
* Fix: Auto-updater was not scheduled when enabling the setting unless Tautulli was restarted.
* New: Docker images updated to support ARM platforms.
* Change: Remove the unnecessary optional Plex logs volume from the Docker image.
* Change: Use Plex.tv for GeoIP lookup instead of requiring the MaxMind GeoLite2 database.
## v2.2.1 (2020-03-28)
* Notifications:
* Fix: File size notification parameter incorrectly truncated to an integer.
* Fix: Notification grouping by season/album not enabled by default.
* New: Added transcode decision counts to notification parameters.
* Change: Tags (<>) are no longer stripped from from Webhook notification text.
* Newsletter:
* New: Added favicon to newsletter template when viewing as a web page.
* UI:
* Fix: Username missing from the Synced Items table.
* Fix: Windows system tray icon not enabled by default.
* Fix: Saving a mobile device with a blank friendly name caused an error.
* New: Added IMDb and Rotten Tomato Ratings to info pages.
* New: Added button in settings to delete all 3rd party metadata lookup info in the database.
* New: Added button in settings to flush recently added items in the database.
* API:
* New: Added delete_recenly_added API command to flush recently added items.
* Change: Updated delete_lookup_info API command parameters to allow deleteing all 3rd party metadata lookup info.
## v2.2.0 (2020-03-08)
* Important Note!
* All Live TV changes requires Plex Media Server 1.18.7 or higher.
* Monitoring:
* Fix: Improved IPv6 display on the activity cards. (Thanks @felixbuenemann)
* New: Added Live TV metadata and posters to the activity cards.
* Change: Show bandwidth in Gbps when greater than 1000 Mbps.
* History:
* New: Added history logging for Live TV sessions.
* New: Added a fake "Live TV" library to collect Live TV history.
* Note: This library will show up the first time that Live TV is played.
* New: Added the ability to filter history by Live TV.
* Graphs:
* New: Added Live TV series to the "Plays by Period" and "Play Totals" graphs.
* Change: Media type series on the graphs are only shown if the corresponding library type is present.
* Notifications:
* Fix: Race condition causing stream count to be incorrect for playback stop notifications.
* New: Added Live TV channel notification parameters.
* New: Added Plex background art notification parameter.
* Note: This is the Plex API endpoint to retrieve the background art, not the actual image.
* New: Added poster images for clip notifications.
* Change: Default Webhook notification method to POST.
* UI:
* Fix: Windows platform showing up twice on the Most Active Platforms statistics card.
* New: Added option to change the background art for library sections when editing a library.
* New: Added button to reset Tautulli git installation in settings to fix failed git updates.
* API:
* New: Added ability to filter history using a "live" media type and by guid for the get_history API command.
* New: Added cutsom_art parameter to the edit_library API command.
* Other:
* Change: Add crossorigin use-credentials attribute to manifest tags. (Thanks @pkoenig10)
* Change: Disable automatic updates for Docker containers. Updates are now handled by updating the Docker container.
* Note: If you are using an old Docker container created before v2.2.0, then you may need to completely remove and recreate the container to update for the first time.
* Note: Use the ":latest" Docker tag for the newest stable release, or the ":beta" or ":nightly" tags to access the beta or nightly branches.
## v2.1.44 (2020-02-05)
* Monitoring:
* Fix: SDR source video being identified as HDR stream video.
* Notifications:
* Fix: Unable to select condition operator for video color parameters.
* UI:
* Fix: Capitalization for platforms on history tables.
## v2.1.43 (2020-02-03)
* Monitoring:
* New: Added HDR indicator on activity card.
* New: Added dynamic range to history steam info modal.
* Notifications:
* Fix: Webhook notification body sent as incorrect data type when Content-Type header is overridden.
* Fix: Telegram notification character limit incorrect for unicode characters.
* New: Added color and dynamic range notification parameters.
* Newsletters:
* Fix: Episodes and Albums plural spelling on recently added newsletter section headers.
* UI:
* Fix: Windows and macOS platform capitalization.
* Fix: Season number 0 not shown for episodes on history tables.
* Other:
* Change: Mask email addresses in logs.
* Change: Update deprecated GitHub access token URL parameter to Authorization header.
## v2.1.42 (2020-01-04)
* Other:
* Fix: SSL certificate error when installing GeoLite2 database.
* Change: Verify MaxMind license key and GeoLite2 database path before installing.
* Change: Disable GeoLite2 database uninstall button when it is not installed.
## v2.1.41 (2019-12-30)
* Other:
* Fix: Failing to extract the GeoLite2 database on Windows.
## v2.1.40 (2019-12-30)
* UI:
* Change: Moved 3rd Party API settings to new tab in the settings.
* Graphs:
* Change: Improve calculating month ranges for Play Totals graphs.
* Other:
* Fix: Failing to verify a Plex Media Server using a hostname.
* Change: A license key is now required to install the MaxMind GeoLite2 database for IP geolocation. Please follow the guide in the wiki to reinstall the GeoLite2 database.
* Change: The GeoLite2 database will now automatically update periodically if installed.
## v2.1.39 (2019-12-08)
* UI:
* New: Added creating admin username and password to setup wizard.
* API:
* Change: Remove default notification subject and body for notify API command.
* Other:
* Change: Check for database corruption when making backup.
## v2.1.38 (2019-11-17)
* Notifications:
* New: Added custom JSON headers to the webhook notification agent.
* UI:
* Fix: Homepage recently watched card not showing grouped history.
* Other:
* New: Added GitHub sponsor donation option.
* Change: Improve resolving hostnames.
## v2.1.37 (2019-10-11)
* Notifications:
* Fix: Last.fm URLs linking to artist page instead of the album page.
* New: Added option for MusicBrainz lookup for music notifications. Option must be enabled under 3rd Party APIs in the settings.
* New: Added MusicBrainz ID and MusicBrainz URL notification parameters.
* Change: Automatically truncate Discord description summary to 2048 characters.
## v2.1.36-beta (2019-10-05)
* Monitoring:
* Fix: Activity card title not updating after pre-rolls or auto-play.
* History:
* Fix: Display correct interlaced or progressive video scan type on stream data modal.
* Graphs:
* New: Separate interlaced and progressive video scan type on source and stream resolution graphs.
* API:
* New: Added parent_guid and grandparent_guid to get_activity and get_metadata commands.
## v2.1.35-beta (2019-09-24)
* Monitoring:
* Fix: Audio shown as blank on activity cards when changing audio tracks during direct play.
* Fix: Display correct interlaced or progressive video scan type on activity cards.
* New: Added flag for Nvidia hardware decoding on activity cards.
* Notifications:
* Fix: Notification parameter prefix and suffix were not substituted correctly.
* Fix: Release Date notification parameter was incorrectly casted to an integer instead of a string.
* New: Added video scan type and full resolution notification parameters.
* UI:
* Fix: Movies with the same title but different year being grouped on the homepage stats cards.
* API:
* New: Added video scan type and full resolution values to get_activity command.
* Other:
* Fix: Tautulli logging out every time after saving settings and restarting.
## v2.1.34 (2019-09-03)
* History:
* New: Added Product column to history tables.
* Notifications:
* Fix: IMDB/TMDb/TVDB/TVmaze ID notification parameters showing blank values after lookup.
* UI:
* Fix: Libraries and Users tables did not respect the group history setting.
* API:
* Fix: Title field was not searchable in get_library_media_info command.
* New: Added grouping option to get_libraries_table and get_users_table commands.
* New: Added product value to get_history command.
* Other:
* Fix: Could not verify Plex Media Server with unpublished hostnames.
* Change: Automatically logout all Tautulli instances when changing the admin password.
## v2.1.33 (2019-07-27)
* Notifications:
* Change: Mask notification agent password fields.
* Change: Enable searching by email address in dropdown menu.
* Other:
* Fix: Version number being overwritten with "None" which prevented updating in some instances.
* Change: Update Plex OAuth request headers.
## v2.1.32 (2019-06-26)
* Newsletters:
* Fix: Newsletter scheduler issue for QNAP devices using an invalid "local" timezone preventing Tautulli from starting.
## v2.1.31 (2019-06-24)
* No additional changes from v2.1.31-beta.
## v2.1.31-beta (2019-06-13)
* Monitoring:
* Fix: Synced content showing incorrect stream info.
* Other:
* Fix: Unable to view database status when authentication is enabled.
* Change: Default database synchronous mode changed to prevent database corruption. Database response may be slower.
## v2.1.30-beta (2019-05-11)
* Monitoring:
* Fix: Activity crashing with Plex's Artist TV feature.
* New: Added setting for Plex Media Server Update Check Interval. (Thanks @abiacco)
* Notifications:
* New: Added secure and relayed connection notification parameters.
* New: Added PLEX_USER_TOKEN to script environment variables.
* Change: Schedule notifications using UTC to prevent missing notifications due to misconfigured timezones.
* API:
* New: Added status API command to check the status of Tautulli.
## v2.1.29 (2019-05-11)
* No additional changes from v2.1.29-beta.
## v2.1.29-beta (2019-04-14)
* Monitoring:
* Change: "Required Bandwidth" changed to "Reserved Bandwidth" in order to match the Plex dashboard.
* Notifications:
* New: Added prefix and suffix notification text modifiers. See the "Notification Text Modifiers" help modal for details.
* UI:
* New: Added "Undelete" button to the edit library and edit user modals.
* Fix: User IP address history table showing incorrect "Last Seen" values.
* API:
* Fix: Search API only returning 3 results.
* Fix: Terminate stream API failing when both session_key and session_id were provided.
* Change: Improved API response HTTP status codes and error messages.
## v2.1.28 (2019-03-10)
* Monitoring:
* New: Added secure/insecure connection icon on the activity cards. Requires Plex Media Server v1.15+.
* Other:
* Change: Improved mass deleting of all images from Cloudinary. Requires all previous images on Cloudinary to be manually tagged with "tautulli". New uploads are automatically tagged.
## v2.1.27-beta (2019-03-03)
* Monitoring:
* Fix: Error when playing synced optimized versions.
* Change: Show message to complete the setup wizard instead of error communicating with server message.
* Change: URL changed on Plex.tv for Plex Media Server beta updates.
* Notifications:
* New: Show the media type exclusion tags in the text preview modal.
* Fix: Unicode error in the Email notification failed response message.
* Fix: Error when a notification agent response is missing the "Content-Type" header.
* UI:
* Fix: Usernames were not being sanitized in dropdown selectors.
* Change: Different display of "All" recently added items on the homepage due to change in the Plex Media Server v1.15+ API.
* API:
* New: Added current Tautulli version to update_check API response.
* Change: API no longer returns sanitized HTML response data.
* Other:
* New: Added auto-restart to systemd init script.
* Fix: Patreon donation URL.
* Remove: Crypto donation options.
## v2.1.26 (2018-12-01)
* Monitoring:
* Fix: Resume event not being triggered after buffering.
* Notifications:
* New: Added user email as a notification parameter.
* Graphs:
* Fix: History model showing no results for stream info graph.
* API:
* Fix: API returning error when missing a cmd.
## v2.1.25 (2018-11-03)
* Monitoring:
* Fix: Audio and video codec showing up as * on the activity cards.
* New: Poster and background image on the activity cards for live TV.
* UI:
* Fix: Alert message for invalid Tautulli Public Domain setting.
## v2.1.24-beta (2018-10-29)
* Monitoring:
* Fix: Transcode change events creating invalid sessions in the database.
* Notifications:
* Change: Update Telegram character limit to 1024.
* History:
* Fix: Save history table states separately for multiple Tautulli instances.
* Graphs:
* Fix: Save graphs states separately for multiple Tautulli instances.
* Change: Version graphs to bypass browser cache.
* UI:
* New: Added queued tasks modals to the scheduled tasks table for debugging.
* Other:
* Change: Updated timezone info and display in configuration table.
## v2.1.23-beta (2018-10-14) ## v2.1.23-beta (2018-10-14)
* Monitoring: * Monitoring:
@@ -58,7 +545,8 @@
## v2.1.20 (2018-09-05) ## v2.1.20 (2018-09-05)
* No changes.
* No additional changes from v2.1.20-beta.
## v2.1.20-beta (2018-09-02) ## v2.1.20-beta (2018-09-02)

View File

@@ -9,7 +9,7 @@ All pull requests should be based on the `nightly` branch, to minimize cross mer
### Python Code ### Python Code
#### Compatibility #### Compatibility
The code should work with Python 2.7. Note that Tautulli runs on many different platforms. The code should work with Python 2.7.17 or Python 3.6+. Note that Tautulli runs on many different platforms.
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. 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.
@@ -38,4 +38,4 @@ HTML5 compatible browsers are targeted.
* 4 space indentation * 4 space indentation
* `methodName` * `methodName`
* `variableName` * `variableName`
* `ClassName` * `ClassName`

23
Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM tautulli/tautulli-baseimage:python3
LABEL maintainer="Tautulli"
ARG BRANCH
ARG COMMIT
ENV TAUTULLI_DOCKER=True
ENV TZ=UTC
WORKDIR /app
RUN \
echo ${BRANCH} > /app/branch.txt && \
echo ${COMMIT} > /app/version.txt
COPY . /app
ENTRYPOINT [ "./start.sh" ]
VOLUME /config
EXPOSE 8181
HEALTHCHECK --start-period=90s CMD curl -ILfSs http://localhost:8181/status > /dev/null || curl -ILfkSs https://localhost:8181/status > /dev/null || exit 1

View File

@@ -1,41 +0,0 @@
<!---
Reporting Issues:
* To ensure that a developer has enough information to work with please include all of the information below.
Please provide as much detail as possible. Screenshots can be very useful to see the problem.
* Use proper markdown syntax to structure your post (i.e. code/log in code blocks).
See: https://help.github.com/articles/basic-writing-and-formatting-syntax/
* Include a link to your **FULL** log file that has the error(not just a few lines!).
Please use [Gist](http://gist.github.com) or [Pastebin](http://pastebin.com/).
Feature Requests:
* Feature requests are handled on FeatHub: http://feathub.com/Tautulli/Tautulli
* Do not post them on the GitHub issues tracker.
-->
**Version:**
**Branch:**
**Commit hash:**
**Operating system:**
**Python version:**
**What you did?**
**What happened?**
**What you expected?**
**How can we reproduce your issue?**
<!-- Provide a detailed step-by-step. -->
**What are your (relevant) settings?**
**Link to logs:**
<!--
Close your issue when it's solved! If you found the solution yourself please comment so that others benefit from it.
-->

View File

@@ -1,8 +1,4 @@
#!/bin/sh #!/usr/bin/env python
''''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 -*- # -*- coding: utf-8 -*-

View File

@@ -1,9 +1,5 @@
# Tautulli # Tautulli
[![Discord](https://img.shields.io/badge/Discord-Tautulli-7289DA.svg?style=flat-square)](https://tautulli.com/discord)
[![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/t/tautulli-monitor-your-plex-media-server/225242)
A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv). A python based web application for monitoring, analytics and notifications for [Plex Media Server](https://plex.tv).
This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb). This project is based on code from [Headphones](https://github.com/rembo10/headphones) and [PlexWatchWeb](https://github.com/ecleese/plexWatchWeb).
@@ -31,7 +27,22 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
![Tautulli Homepage](https://tautulli.com/images/screenshots/activity-compressed.jpg?v=2) ![Tautulli Homepage](https://tautulli.com/images/screenshots/activity-compressed.jpg?v=2)
## Installation and Support ## Installation & Support
[![Python](https://img.shields.io/badge/python-2.7.17,%203.6,%203.7,%203.8-blue?style=flat-square)](https://python.org/downloads)
[![Docker Pulls](https://img.shields.io/docker/pulls/tautulli/tautulli?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
[![Docker Stars](https://img.shields.io/docker/stars/tautulli/tautulli?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli)
| Status | Branch: `master` | Branch: `beta` | Branch: `nightly` |
| --- | --- | --- | --- |
| Release | [![Release@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![Release Date@master](https://img.shields.io/github/release-date/Tautulli/Tautulli?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/releases/latest) | [![Release@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![Commits@beta](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/beta?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/beta) | [![Last Commits@nightly](https://img.shields.io/github/last-commit/Tautulli/Tautulli/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) <br> [![Commits@nightly](https://img.shields.io/github/commits-since/Tautulli/Tautulli/latest/nightly?style=flat-square&color=blue)](https://github.com/Tautulli/Tautulli/commits/nightly) |
| Docker | [![Docker@master](https://img.shields.io/badge/docker-latest-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Amaster) | [![Docker@beta](https://img.shields.io/badge/docker-beta-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@beta](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Abeta) | [![Docker@nightly](https://img.shields.io/badge/docker-nightly-blue?style=flat-square)](https://hub.docker.com/r/tautulli/tautulli) <br> [![Docker Build@nightly](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Docker/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Docker"+branch%3Anightly) |
| Installer | [![Windows@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=windows&style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![MacOS@master](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=macos&style=flat-square)](https://github.com/Tautulli/Tautulli/releases/latest) <br> [![Installer Build@master](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Release/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Amaster) | [![Windows@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=windows&include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![MacOS@beta](https://img.shields.io/github/v/release/Tautulli/Tautulli?label=macos&include_prereleases&style=flat-square)](https://github.com/Tautulli/Tautulli/releases) <br> [![Installer Build@beta](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Release/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Abeta) | [![Installer Build@nightly](https://img.shields.io/github/workflow/status/Tautulli/Tautulli/Publish%20Release/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=workflow%3A"Publish+Release"+branch%3Anightly) |
[![Wiki](https://img.shields.io/badge/github-wiki-black?style=flat-square)](https://github.com/Tautulli/Tautulli-Wiki/wiki)
[![Discord](https://img.shields.io/discord/183396325142822912?label=discord&style=flat-square&color=7289DA)](https://tautulli.com/discord)
[![Reddit](https://img.shields.io/reddit/subreddit-subscribers/tautulli?label=reddit&style=flat-square&color=FF5700)](https://www.reddit.com/r/Tautulli/)
[![Plex Forums](https://img.shields.io/badge/plex%20forums-discussion-E5A00D?style=flat-square)](https://forums.plex.tv/t/tautulli-monitor-your-plex-media-server/225242)
* Read the [Installation Guides](https://github.com/Tautulli/Tautulli-Wiki/wiki/Installation) for instructions to install Tautulli. * 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. * The [Frequently Asked Questions](https://github.com/Tautulli/Tautulli-Wiki/wiki/Frequently-Asked-Questions) in the wiki can help you with common problems.
@@ -39,10 +50,15 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
## Issues & Feature Requests ## Issues & Feature Requests
[![Issues](https://img.shields.io/badge/github-issues-red?style=flat-square)](https://github.com/Tautulli/Tautulli-Issues)
[![Feathub](https://img.shields.io/badge/feathub-requests-lightgrey?style=flat-square)](https://feathub.com/Tautulli/Tautulli)
* Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues). * Please see the [Issues Repository](https://github.com/Tautulli/Tautulli-Issues).
## License ## License
[![License](https://img.shields.io/github/license/Tautulli/Tautulli?style=flat-square)](https://github.com/Tautulli/Tautulli/blob/master/LICENSE)
This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included. This is free software under the GPL v3 open source license. Feel free to do with it what you wish, but any modification must be open sourced. A copy of the license is included.
This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution. This software includes Highsoft software libraries which you may freely distribute for non-commercial use. Commerical users must licence this software, for more information visit https://shop.highsoft.com/faq/non-commercial#non-commercial-redistribution.

View File

@@ -1,8 +1,4 @@
#!/bin/sh #!/usr/bin/env python
''''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 -*- # -*- coding: utf-8 -*-
@@ -27,14 +23,24 @@ import sys
# Ensure lib added to path, before any other imports # Ensure lib added to path, before any other imports
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib')) sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), 'lib'))
from future.builtins import str
import appdirs
import argparse import argparse
import datetime
import locale import locale
import pytz
import signal import signal
import time import time
import threading
import tzlocal
import plexpy import plexpy
from plexpy import config, database, logger, webstart from plexpy import common, config, database, helpers, logger, webstart
if common.PLATFORM == 'Windows':
from plexpy import windows
elif common.PLATFORM == 'Darwin':
from plexpy import macos
# Register signals, such as CTRL + C # Register signals, such as CTRL + C
signal.signal(signal.SIGINT, plexpy.sig_handler) signal.signal(signal.SIGINT, plexpy.sig_handler)
@@ -48,12 +54,14 @@ def main():
""" """
# Fixed paths to Tautulli # Fixed paths to Tautulli
if hasattr(sys, 'frozen'): if hasattr(sys, 'frozen') and hasattr(sys, '_MEIPASS'):
plexpy.FROZEN = True
plexpy.FULL_PATH = os.path.abspath(sys.executable) plexpy.FULL_PATH = os.path.abspath(sys.executable)
plexpy.PROG_DIR = sys._MEIPASS
else: else:
plexpy.FULL_PATH = os.path.abspath(__file__) plexpy.FULL_PATH = os.path.abspath(__file__)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.PROG_DIR = os.path.dirname(plexpy.FULL_PATH)
plexpy.ARGS = sys.argv[1:] plexpy.ARGS = sys.argv[1:]
# From sickbeard # From sickbeard
@@ -103,20 +111,27 @@ def main():
plexpy.QUIET = True plexpy.QUIET = True
# Do an intial setup of the logger. # Do an intial setup of the logger.
logger.initLogger(console=not plexpy.QUIET, log_dir=False, # Require verbose for pre-initilization to see critical errors
verbose=plexpy.VERBOSE) logger.initLogger(console=not plexpy.QUIET, log_dir=False, verbose=True)
if os.getenv('TAUTULLI_DOCKER', False) == 'True': try:
plexpy.SYS_TIMEZONE = tzlocal.get_localzone()
except (pytz.UnknownTimeZoneError, LookupError, ValueError) as e:
logger.error("Could not determine system timezone: %s" % e)
plexpy.SYS_TIMEZONE = pytz.UTC
plexpy.SYS_UTC_OFFSET = datetime.datetime.now(plexpy.SYS_TIMEZONE).strftime('%z')
if helpers.bool_true(os.getenv('TAUTULLI_DOCKER', False)):
plexpy.DOCKER = True plexpy.DOCKER = True
if args.dev: if args.dev:
plexpy.DEV = True plexpy.DEV = True
logger.debug(u"Tautulli is running in the dev environment.") logger.debug("Tautulli is running in the dev environment.")
if args.daemon: if args.daemon:
if sys.platform == 'win32': if sys.platform == 'win32':
sys.stderr.write( logger.warn("Daemonizing not supported under Windows, starting normally")
"Daemonizing not supported under Windows, starting normally\n")
else: else:
plexpy.DAEMON = True plexpy.DAEMON = True
plexpy.QUIET = True plexpy.QUIET = True
@@ -134,11 +149,13 @@ def main():
try: try:
with open(plexpy.PIDFILE, 'r') as fp: with open(plexpy.PIDFILE, 'r') as fp:
pid = int(fp.read()) pid = int(fp.read())
os.kill(pid, 0)
except IOError as e: except IOError as e:
raise SystemExit("Unable to read PID file: %s", e) raise SystemExit("Unable to read PID file: %s", e)
try:
os.kill(pid, 0)
except OSError: except OSError:
logger.warn("PID file '%s' already exists, but PID %d is " \ logger.warn("PID file '%s' already exists, but PID %d is "
"not running. Ignoring PID file." % "not running. Ignoring PID file." %
(plexpy.PIDFILE, pid)) (plexpy.PIDFILE, pid))
else: else:
@@ -164,6 +181,8 @@ def main():
# Determine which data directory and config file to use # Determine which data directory and config file to use
if args.datadir: if args.datadir:
plexpy.DATA_DIR = args.datadir plexpy.DATA_DIR = args.datadir
elif plexpy.FROZEN:
plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
else: else:
plexpy.DATA_DIR = plexpy.PROG_DIR plexpy.DATA_DIR = plexpy.PROG_DIR
@@ -218,37 +237,51 @@ def main():
try: try:
import OpenSSL import OpenSSL
except ImportError: except ImportError:
logger.warn("The pyOpenSSL module is missing. Install this " \ logger.warn("The pyOpenSSL module is missing. Install this "
"module to enable HTTPS. HTTPS will be disabled.") "module to enable HTTPS. HTTPS will be disabled.")
plexpy.CONFIG.ENABLE_HTTPS = False plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use. # Try to start the server. Will exit here is address is already in use.
web_config = { webstart.start()
'http_port': plexpy.HTTP_PORT,
'http_host': plexpy.CONFIG.HTTP_HOST, if common.PLATFORM == 'Windows':
'http_root': plexpy.CONFIG.HTTP_ROOT, if plexpy.CONFIG.SYS_TRAY_ICON:
'http_environment': plexpy.CONFIG.HTTP_ENVIRONMENT, plexpy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
'http_proxy': plexpy.CONFIG.HTTP_PROXY, plexpy.WIN_SYS_TRAY_ICON.start()
'enable_https': plexpy.CONFIG.ENABLE_HTTPS, windows.set_startup()
'https_cert': plexpy.CONFIG.HTTPS_CERT, elif common.PLATFORM == 'Darwin':
'https_cert_chain': plexpy.CONFIG.HTTPS_CERT_CHAIN, macos.set_startup()
'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 # Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV: if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT, plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT,
plexpy.HTTP_ROOT) plexpy.HTTP_ROOT)
# Windows system tray icon if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
if os.name == 'nt' and plexpy.CONFIG.WIN_SYS_TRAY: if not macos.HAS_PYOBJC:
plexpy.win_system_tray() logger.warn("The pyobjc module is missing. Install this "
"module to enable the MacOS menu bar icon.")
plexpy.CONFIG.SYS_TRAY_ICON = False
# Wait endlessy for a signal to happen if plexpy.CONFIG.SYS_TRAY_ICON:
# MacOS menu bar icon must be run on the main thread and is blocking
# Start the rest of Tautulli on a new thread
thread = threading.Thread(target=wait)
thread.daemon = True
thread.start()
plexpy.MAC_SYS_TRAY_ICON = macos.MacOSSystemTray()
plexpy.MAC_SYS_TRAY_ICON.start()
else:
wait()
else:
wait()
def wait():
logger.info("Tautulli is ready!")
# Wait endlessly for a signal to happen
while True: while True:
if not plexpy.SIGNAL: if not plexpy.SIGNAL:
try: try:
@@ -264,11 +297,16 @@ def main():
plexpy.shutdown(restart=True) plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout': elif plexpy.SIGNAL == 'checkout':
plexpy.shutdown(restart=True, checkout=True) plexpy.shutdown(restart=True, checkout=True)
else: elif plexpy.SIGNAL == 'reset':
plexpy.shutdown(restart=True, reset=True)
elif plexpy.SIGNAL == 'update':
plexpy.shutdown(restart=True, update=True) plexpy.shutdown(restart=True, update=True)
else:
logger.error('Unknown signal. Shutting down...')
plexpy.shutdown()
plexpy.SIGNAL = None plexpy.SIGNAL = None
# Call main()
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# Display information # Display information
echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated." echo "This script will remove *.pyc files. These files are generated by Python, but they can cause conflicts after an upgrade. It's safe to remove them, because they will be regenerated."

View File

@@ -1,4 +1,4 @@
#!/bin/bash #!/usr/bin/env bash
# Parameter check # Parameter check
if [ -z "$1" ]; then if [ -z "$1" ]; then

View File

@@ -5,46 +5,105 @@
<h4 class="modal-title">Import ${app} Database</h4> <h4 class="modal-title">Import ${app} Database</h4>
</div> </div>
<div class="modal-body" id="modal-text"> <div class="modal-body" id="modal-text">
<p class="help-block"> <form id="import_database_form" enctype="multipart/form-data" method="post" name="import_database_form">
<% <input type="hidden" id="import_app" name="import_app" value="${app.lower()}" />
v = '' % if app in ('PlexWatch', 'Plexivity'):
if app == 'PlexWatch': <p class="help-block">
v = '0.3.2' <%
elif app == 'Plexivity': v = ''
v = '0.9.8' if app == 'PlexWatch':
%> v = '0.3.2'
<strong>Please ensure your ${app} database is at version ${v} or higher.</strong> elif app == 'Plexivity':
</p> v = '0.9.8'
<div class="form-group"> %>
<label for="db_location">Database Location</label> <strong>Please ensure your ${app} database is at version ${v} or higher.</strong>
<div class="row"> </p>
<div class="col-xs-8"> % endif
<input type="text" class="form-control" id="db_location" name="db_location" value="" required> <div class="form-group">
<label for="import_database_file">Option 1: Upload a Database File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<label for="import_database_file" class="input-group-btn">
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_database_file" name="import_database_file" required>
</label>
<input id="import_database_file_name" type="text" class="form-control" placeholder="tautulli.db" disabled>
</div>
</div>
</div> </div>
<p class="help-block">Upload the ${app} database file you wish to import.</p>
</div> </div>
<p class="help-block">Enter the path and file name for the ${app} database you wish to import.</p> <div class="form-group">
</div> <label for="import_database_path">Option 2: Browse for a Database File</label>
<div class="form-group"> <div class="row">
<label for="table_name">Table Name</label> <div class="col-xs-12">
<div class="row"> <div class="input-group">
<div class="col-xs-4"> <span class="input-group-btn">
<select id="table_name" class="form-control" name="table_name"> <button class="btn btn-form" type="button" id="import_database_path_browse" data-toggle="browse" data-description="Database File" data-filter=".db" data-target="#import_database_path">Browse</button>
<option value="processed">processed</option> </span>
<option value="grouped">grouped</option> <input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" placeholder="tautulli.db" required disabled>
</select> </div>
</div>
</div> </div>
<p class="help-block">Browse for the ${app} database file you wish to import.</p>
</div> </div>
<p class="help-block">The table name from which you wish to import. Only import one of these, importing both will result in duplicated data.</p> % if app == 'Tautulli':
</div> <div class="form-group">
<div class="form-group"> <label for="table_name">Import Method</label>
<label for="import_ignore_interval">Ignore Interval</label> <div class="row">
<div class="row"> <div class="col-xs-4">
<div class="col-xs-2"> <select class="form-control" id="import_method" name="import_method">
<input type="text" class="form-control" id="import_ignore_interval" name="import_ignore_interval" value="120" required> <option value="merge">Merge</option>
<option value="overwrite">Overwrite</option>
</select>
</div>
</div> </div>
<p class="help-block">Select how you would like to import the Tautulli history.</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li><strong>Merge</strong> will add all history and remove any duplicates from the imported database into the current database.</li>
<li><strong>Overwrite</strong> will replace all history in the current database with the imported database.</li>
</ul>
</div> </div>
<p class="help-block">Enter the minimum duration (in seconds) an item must have been active for. Set to 0 to import all.</p> <div class="checkbox">
</div> <label>
<input type="checkbox" name="import_backup_db" id="import_backup_db" value="1" checked> Backup Current Database
</label>
<p class="help-block">Automatically create a backup of the current database before importing.</p>
</div>
<div class="form-group">
<label>Import Notes</label>
<p class="help-block">The following data will also be imported:</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li>Libraries and Users</li>
<li>Notification / Newsletter Agents</li>
<li>Registered Mobile Devices</li>
</ul>
</div>
% else:
<div class="form-group">
<label for="import_table_name">Table Name</label>
<div class="row">
<div class="col-xs-4">
<select class="form-control" id="import_table_name" name="import_table_name">
<option value="processed">Processed</option>
<option value="grouped">Grouped</option>
</select>
</div>
</div>
<p class="help-block">Select the table name from which you wish to import. Only import one of these, importing both will result in duplicated data.</p>
</div>
<div class="form-group">
<label for="import_ignore_interval">Ignore Interval</label>
<div class="row">
<div class="col-xs-2">
<input type="text" class="form-control" id="import_ignore_interval" name="import_ignore_interval" value="120" required>
</div>
</div>
<p class="help-block">Enter the minimum duration (in seconds) an item must have been active for. Set to 0 to import all.</p>
</div>
% endif
</form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<div> <div>
@@ -55,24 +114,87 @@
</div> </div>
</div> </div>
<script> <script>
// Send database path to import script $("#import_database_file").change(function() {
if ($(this)[0].files[0]) {
$("#import_database_file_name").val($(this)[0].files[0].name);
}
});
$("#import_db").click(function() { $("#import_db").click(function() {
var database_path = $("#db_location").val(); $(this).prop('disabled', true);
var table_name = $("#table_name").val();
var import_ignore_interval = $("#import_ignore_interval").val(); var app = $("#import_app").val();
var database_file = $("#import_database_file")[0].files[0];
var database_path = $("#import_database_path").val();
var method = $("#import_method").val();
var backup = $("#import_backup_db").is(':checked');
var table_name = $("#import_table_name").val();
var ignore_interval = $("#import_ignore_interval").val();
var content_type;
var process_data;
var data;
if (database_file) {
content_type = false;
process_data = false;
data = new FormData();
data.append('app', app);
data.append('database_file', database_file);
data.append('method', method);
data.append('backup', backup);
data.append('table_name', table_name);
data.append('ignore_interval', ignore_interval);
} else {
content_type = 'application/x-www-form-urlencoded; charset=UTF-8';
process_data = true;
data = {
app: app,
database_path: database_path,
method: method,
backup: backup,
table_name: table_name,
ignore_interval: ignore_interval
}
}
if (database_file) {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>&nbsp; Uploading database file...');
} else {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
}
$.ajax({ $.ajax({
url: 'import_database', url: 'import_database',
data: { type: 'POST',
app: "${app}", data: data,
database_path: database_path,
table_name: table_name,
import_ignore_interval: import_ignore_interval
},
cache: false, cache: false,
async: true, async: true,
contentType: content_type,
processData: process_data,
success: function(data) { success: function(data) {
$("#status-message").html(data); var msg;
$("#db_location").val('') if (data.result === 'success') {
msg = "<i class='fa fa-check'></i>&nbsp; " + data.message;
} else {
msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; " + data.message;
}
$("#status-message").html(msg);
$("#import_database_file").val(null);
$("#import_database_file_name").val('');
$("#import_database_path").val('');
},
error: function (xhr) {
var msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; Error (" + xhr.status + "): ";
if (xhr.status === 413) {
msg += "file is too large to upload"
} else {
msg += 'try again'
}
$("#status-message").html(msg);
},
complete: function(xhr) {
$("#import_db").prop('disabled', false);
} }
}); });
}); });

View File

@@ -28,7 +28,7 @@
<!-- ICONS --> <!-- ICONS -->
<!-- Android --> <!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5"> <link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<meta name="theme-color" content="#282a2d"> <meta name="theme-color" content="#282a2d">
<!-- Apple --> <!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5"> <link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
@@ -43,23 +43,25 @@
<div class="container"> <div class="container">
<div id="ajaxMsg" class="ajaxMsg"></div> <div id="ajaxMsg" class="ajaxMsg"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
% if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is None: % if plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE is not False:
<div id="updatebar" style="display: none;"> <div id="updatebar" style="display: none;">
% if plexpy.UPDATE_AVAILABLE is None:
You are running an unknown version of Tautulli.<br /> You are running an unknown version of Tautulli.<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> % elif plexpy.UPDATE_AVAILABLE == 'release':
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE == 'release':
<div id="updatebar" style="display: none;">
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"> 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 /> new release (${plexpy.LATEST_RELEASE})</a> of Tautulli is available!<br />
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> % elif plexpy.UPDATE_AVAILABLE == 'commit':
</div>
% elif plexpy.CONFIG.CHECK_GITHUB and plexpy.UPDATE_AVAILABLE == 'commit':
<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"> 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 /> newer version</a> of Tautulli is available!<br />
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br /> You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
% endif
% if plexpy.INSTALL_TYPE == 'docker':
Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>
% elif plexpy.INSTALL_TYPE in ('windows', 'macos'):
<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">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>
% else:
<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a> <a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>
% endif
</div> </div>
% else: % else:
<div id="updatebar" style="display: none;"></div> <div id="updatebar" style="display: none;"></div>
@@ -209,7 +211,7 @@ ${next.modalIncludes()}
</div> </div>
</div> </div>
% else: % else:
<div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="crypto-donate-modal"> <div id="donate-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="donate-modal">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
@@ -228,19 +230,24 @@ ${next.modalIncludes()}
</div> </div>
</div> </div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;"> <ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li> <li class="active"><a href="#github-donation" role="tab" data-toggle="tab">GitHub</a></li>
<li><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="#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> </ul>
<div class="tab-content"> <div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center"> <div role="tabpanel" class="tab-pane active" id="github-donation" style="text-align: center">
<p>
Click the button below to continue to GitHub.
</p>
<a href="${anon_url('https://github.com/sponsors/JonnyWong16')}" target="_blank" class="btn btn-sm btn-default" style="font-weight: 600;">
<i class="fa fa-heart fa-sm" style="color: #ea4aaa;"></i>&nbsp; Sponsor
</a>
</div>
<div role="tabpanel" class="tab-pane" id="patreon-donation" style="text-align: center">
<p> <p>
Click the button below to continue to Patreon. Click the button below to continue to Patreon.
</p> </p>
<a href="${anon_url('https://www.patreon.com/bePatron?u=10078609')}" target="_blank"> <a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40"> <img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a> </a>
</div> </div>
@@ -252,12 +259,6 @@ ${next.modalIncludes()}
<img src="images/gold-rect-paypal-34px.png" alt="PayPal"> <img src="images/gold-rect-paypal-34px.png" alt="PayPal">
</a> </a>
</div> </div>
<div role="tabpanel" class="tab-pane" id="crypto-donation">
<label>QR Code</label>
<pre id="crypto_qr_code" style="text-align: center"></pre>
<label><span id="crypto_type_label"></span> Address</label>
<pre id="crypto_address" style="text-align: center"></pre>
</div>
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -292,12 +293,10 @@ ${next.modalIncludes()}
<script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script> <script src="${http_root}js/bootstrap-hover-dropdown.min.js"></script>
<script src="${http_root}js/pnotify.custom.min.js"></script> <script src="${http_root}js/pnotify.custom.min.js"></script>
<script src="${http_root}js/platform.min.js"></script> <script src="${http_root}js/platform.min.js"></script>
<script src="${http_root}js/ipaddr.min.js"></script>
<script src="${http_root}js/script.js${cache_param}"></script> <script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/jquery.qrcode.min.js"></script>
<script src="${http_root}js/jquery.tripleclick.min.js"></script> <script src="${http_root}js/jquery.tripleclick.min.js"></script>
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script> <script src="${http_root}js/ajaxNotifications.js"></script>
% endif
<script> <script>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
$('body').on('click', '#updateDismiss', function() { $('body').on('click', '#updateDismiss', function() {
@@ -320,21 +319,25 @@ ${next.modalIncludes()}
complete: function (xhr, status) { complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = ''; var msg = '';
if (result.update === null) { if (result.update === false) {
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); showMsg('<i class="fa fa-check"></i> ' + result.message, false, true, 2000);
} else {
if (result.update === null) {
msg = 'You are running an unknown version of Tautulli.<br />';
} 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 />';
} 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 />';
}
if (result.install_type === 'docker') {
msg += 'Update your Docker container or <a href="#" id="updateDismiss">Dismiss</a>';
} else if (result.install_type === 'windows' || result.install_type === 'macos') {
msg += '<a href="' + result.release_url + '" target="_blank">Download</a> and install the latest version or <a href="#" id="updateDismiss">Dismiss</a>'
} else {
msg += '<a href="update">Update</a> or <a href="#" id="updateDismiss">Dismiss</a>';
}
$('#updatebar').html(msg).fadeIn();
} }
if (_callback) { if (_callback) {
@@ -365,16 +368,6 @@ ${next.modalIncludes()}
checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); }); checkUpdate(function () { $('#nav-update').html('<i class="fa fa-fw fa-arrow-alt-circle-up"></i> Check for Updates'); });
}); });
$('#donation_type a.crypto-donation').on('shown.bs.tab', function () {
var crypto_coin = $(this).data('coin');
var crypto_name = $(this).data('name');
var crypto_address = $(this).data('address')
$('#crypto_qr_code').empty().qrcode({
text: crypto_coin + ":" + crypto_address
});
$('#crypto_type_label').html(crypto_name);
$('#crypto_address').html(crypto_address);
});
% endif % endif
$('.dropdown-toggle').click(function (e) { $('.dropdown-toggle').click(function (e) {
@@ -428,6 +421,10 @@ ${next.modalIncludes()}
$(document).on('hidden.bs.modal', '.modal', function () { $(document).on('hidden.bs.modal', '.modal', function () {
$('.modal:visible').length && $(document.body).addClass('modal-open'); $('.modal:visible').length && $(document.body).addClass('modal-open');
}); });
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
check_notifications();
% endif
}); });
% if _session['user_group'] != 'admin': % if _session['user_group'] != 'admin':

View File

@@ -0,0 +1,138 @@
<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">${title}</h4>
</div>
<div class="modal-body" id="modal-text">
<form id="import_config_form" enctype="multipart/form-data" method="post" name="import_config_form">
<div class="form-group">
<label for="import_config_file">Option 1: Upload a Configuration File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<label for="import_config_file" class="input-group-btn">
<span class="btn btn-form">Upload</span>
<input type="file" style="display: none;" id="import_config_file" name="import_config_file" required>
</label>
<input id="import_config_file_name" type="text" class="form-control" placeholder="config.ini" disabled>
</div>
</div>
</div>
<p class="help-block">Upload the Tautulli configuration file you wish to import.</p>
</div>
<div class="form-group">
<label for="import_config_path">Option 2: Browse for a Configuration File</label>
<div class="row">
<div class="col-xs-12">
<div class="input-group">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="import_config_path_browse" data-toggle="browse" data-description="Configuration File" data-filter=".ini" data-target="#import_config_path">Browse</button>
</span>
<input type="text" class="form-control" id="import_config_path" name="import_config_path" value="" placeholder="config.ini" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the Tautulli configuration file you wish to import.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="import_backup_config" id="import_backup_config" value="1" checked> Backup Current Configuration
</label>
<p class="help-block">Automatically create a backup of the current configuration before importing.</p>
</div>
<div class="form-group">
<label>Import Notes</label>
<p class="help-block">The following settings will <em>not</em> be imported:</p>
<ul class="help-block" style="padding-inline-start: 15px;">
<li>Git Path, Log / Backup / Cache Directory, Plex Logs Folder</li>
<li>Custom Newsletter Templates Folder, Newsletter Output Directory</li>
<li>HTTP Host / Port / Root / Username / Password</li>
<li>Enable HTTPS, HTTPS Certificate / Certificate Chain / Key</li>
</ul>
</div>
</form>
</div>
<div class="modal-footer">
<div>
<span id="status-message" style="padding-right: 25px;"></span>
<input type="button" id="import_config" class="btn btn-bright" value="Import">
</div>
</div>
</div>
</div>
<script>
$("#import_config_file").change(function() {
if ($(this)[0].files[0]) {
$("#import_config_file_name").val($(this)[0].files[0].name);
}
});
$("#import_config").click(function() {
$(this).prop('disabled', true);
var config_file = $("#import_config_file")[0].files[0];
var config_path = $("#import_config_path").val();
var backup = $("#import_backup_config").is(':checked');
var content_type;
var process_data;
var data;
if (config_file) {
content_type = false;
process_data = false;
data = new FormData();
data.append('config_file', config_file);
data.append('backup', backup);
} else {
content_type = 'application/x-www-form-urlencoded; charset=UTF-8';
process_data = true;
data = {
config_path: config_path,
backup: backup
}
}
if (config_file) {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>&nbsp; Uploading config file...');
} else {
$("#status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
}
$.ajax({
url: 'import_config',
type: 'POST',
data: data,
cache: false,
async: true,
contentType: content_type,
processData: process_data,
success: function(data) {
var msg;
if (data.result === 'success') {
msg = "<i class='fa fa-check'></i>&nbsp; " + data.message;
window.location.href = 'restart_import_config';
} else {
msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; " + data.message;
}
$("#status-message").html(msg);
$("#import_config_file").val(null);
$("#import_config_file_name").val('');
$("#import_config_path").val('');
},
error: function (xhr) {
var msg = "<i class='fa fa-exclamation-triangle'></i>&nbsp; Error (" + xhr.status + "): ";
if (xhr.status === 413) {
msg += "file is too large to upload"
} else {
msg += 'try again'
}
$("#status-message").html(msg);
},
complete: function(xhr) {
$("#import_config").prop('disabled', false);
}
});
});
</script>

View File

@@ -35,7 +35,7 @@ DOCUMENTATION :: END
</tr> </tr>
<tr> <tr>
<td>Database File:</td> <td>Database File:</td>
<td><a class="no-highlight" href="download_database" data-toggle="tooltip" data-placement="right" title="Download Database">${plexpy.DB_FILE}</a></td> <td><a class="no-highlight" href="download_database" data-toggle="tooltip" data-placement="right" title="Download Database">${plexpy.DB_FILE}</a> | <a class="no-highlight" href="status/database">Status</a></td>
</tr> </tr>
<tr> <tr>
<td>Log File:</td> <td>Log File:</td>
@@ -53,14 +53,6 @@ DOCUMENTATION :: END
<td>Newsletter Directory:</td> <td>Newsletter Directory:</td>
<td>${plexpy.CONFIG.NEWSLETTER_DIR}</td> <td>${plexpy.CONFIG.NEWSLETTER_DIR}</td>
</tr> </tr>
<tr>
<td>GeoLite2 Database:</td>
% if plexpy.CONFIG.GEOIP_DB:
<td>${plexpy.CONFIG.GEOIP_DB} | <a class="no-highlight" href="#" id="reinstall_geoip_db">Reinstall / Update</a> | <a class="no-highlight" href="#" id="uninstall_geoip_db">Uninstall</a></td>
% else:
<td><a class="no-highlight" href="#" id="install_geoip_db">Click here to install the GeoLite2 database.</a></td>
% endif
</tr>
% if plexpy.ARGS: % if plexpy.ARGS:
<tr> <tr>
<td>Arguments:</td> <td>Arguments:</td>
@@ -69,7 +61,11 @@ DOCUMENTATION :: END
% endif % endif
<tr> <tr>
<td>Platform:</td> <td>Platform:</td>
<td>${common.PLATFORM} ${common.PLATFORM_RELEASE} (${common.PLATFORM_VERSION + (' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else '')})</td> <td>${'[Docker] ' if plexpy.DOCKER else ''}${common.PLATFORM} ${common.PLATFORM_RELEASE} (${common.PLATFORM_VERSION + (' - {}'.format(common.PLATFORM_LINUX_DISTRO) if common.PLATFORM_LINUX_DISTRO else '')})</td>
</tr>
<tr>
<td>System Timezone:</td>
<td>${plexpy.SYS_TIMEZONE.zone} (${'UTC{}'.format(plexpy.SYS_UTC_OFFSET)})
</tr> </tr>
<tr> <tr>
<td>Python Version:</td> <td>Python Version:</td>
@@ -98,22 +94,6 @@ DOCUMENTATION :: END
<script> <script>
$(document).ready(function () { $(document).ready(function () {
$("#install_geoip_db, #reinstall_geoip_db").click(function () {
var msg = 'Are you sure you want to install the GeoLite2 database?<br /><br />' +
'The database is used to lookup IP address geolocation info.<br />' +
'The database will be downloaded from <a href="${anon_url("https://dev.maxmind.com/geoip/geoip2/geolite2/")}" target="_blank">MaxMind</a>, <br />' +
'and requires <strong>100MB</strong> of free space to install in your Tautulli directory.<br />'
var url = 'install_geoip_db';
confirmAjaxCall(url, msg, null, 'Installing GeoLite2 database.', getConfigurationTable);
});
$("#uninstall_geoip_db").click(function () {
var msg = 'Are you sure you want to uninstall the GeoLite2 database?<br /><br />' +
'You will not be able to lookup IP address geolocation info.';
var url = 'uninstall_geoip_db';
confirmAjaxCall(url, msg, null, 'Uninstalling GeoLite2 database.', getConfigurationTable);
});
$('.guidelines-modal-link').on('click', function (e) { $('.guidelines-modal-link').on('click', function (e) {
e.preventDefault(); e.preventDefault();
$('#guidelines-type').text($(this).data('id')) $('#guidelines-type').text($(this).data('id'))

View File

@@ -21,7 +21,7 @@ ul.ColVis_collection li {
.ColVis_Button:hover, .ColVis_Button:hover,
ul.ColVis_collection li:hover { ul.ColVis_collection li:hover {
color: #F9AA03; color: #E5A00D;
} }
button.ColVis_Button { button.ColVis_Button {

View File

@@ -101,7 +101,7 @@ table.display tr:hover a {
text-decoration:none; text-decoration:none;
} }
table.display td:hover a { table.display td:hover a {
color: #F9AA03; color: #E5A00D;
} }
table.display thead tr:hover { table.display thead tr:hover {
background-color: #212121; background-color: #212121;

View File

@@ -1,6 +1,6 @@
body { body {
font-family: 'Open Sans', Arial, sans-serif; font-family: 'Open Sans', Arial, sans-serif;
color: #fff; color: #eee;
margin-top: 50px; margin-top: 50px;
overflow: hidden; overflow: hidden;
} }
@@ -36,7 +36,7 @@ select.input-sm {
select[multiple] { select[multiple] {
height: 125px; height: 125px;
margin: 5px 0 5px 0; margin: 5px 0 5px 0;
color: #fff; color: #eee;
border: 0px solid #444; border: 0px solid #444;
background: #555; background: #555;
padding: 2px 2px; padding: 2px 2px;
@@ -48,7 +48,7 @@ select[multiple]:focus {
outline: 0; outline: 0;
outline: thin dotted \9; outline: thin dotted \9;
color: #555; color: #555;
background-color: #fff; background-color: #eee;
transition: background-color .3s; transition: background-color .3s;
} }
select[multiple]:focus::-webkit-scrollbar-thumb { select[multiple]:focus::-webkit-scrollbar-thumb {
@@ -63,7 +63,7 @@ select[multiple] option {
select.form-control, select.form-control,
div.form-control .selectize-input { div.form-control .selectize-input {
margin: 5px 0 5px 0; margin: 5px 0 5px 0;
color: #fff; color: #eee;
border: 0px solid #444; border: 0px solid #444;
background: #555; background: #555;
padding: 6px 12px; padding: 6px 12px;
@@ -76,7 +76,7 @@ select.form-control {
} }
.react-selectize.root-node .react-selectize-control, .react-selectize.root-node .react-selectize-control,
.selectize-control.form-control .selectize-input { .selectize-control.form-control .selectize-input {
color: #fff !important; color: #eee !important;
border: 0px solid #444 !important; border: 0px solid #444 !important;
background: #555 !important; background: #555 !important;
padding: 1px 2px; padding: 1px 2px;
@@ -123,15 +123,15 @@ select.form-control {
cursor: pointer; cursor: pointer;
} }
.react-selectize.root-node .react-selectize-control .react-selectize-placeholder { .react-selectize.root-node .react-selectize-control .react-selectize-placeholder {
color: #fff !important; color: #eee !important;
} }
.react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path { .react-selectize.root-node .react-selectize-control .react-selectize-toggle-button path {
fill: #fff !important; fill: #eee !important;
} }
.react-selectize.root-node .simple-value, .react-selectize.root-node .simple-value,
.selectize-control.multi .selectize-input > div { .selectize-control.multi .selectize-input > div {
background: #444444 !important; background: #444 !important;
color: #ffffff !important; color: #eee !important;
padding-bottom: 2px !important; padding-bottom: 2px !important;
transition: background-color .3s; transition: background-color .3s;
} }
@@ -156,7 +156,7 @@ select.form-control:focus,
outline: 0; outline: 0;
outline: thin dotted \9; outline: thin dotted \9;
color: #555 !important; color: #555 !important;
background-color: #fff !important; background-color: #eee !important;
transition: background-color .3s; transition: background-color .3s;
} }
.react-selectize.root-node.open .simple-value, .react-selectize.root-node.open .simple-value,
@@ -219,7 +219,7 @@ select.form-control:focus,
} }
select.form-control option { select.form-control option {
color: #555; color: #555;
background-color: #fff; background-color: #eee;
} }
img { img {
-webkit-box-sizing: content-box; -webkit-box-sizing: content-box;
@@ -278,13 +278,13 @@ object {
} }
.dropdown-menu > li > a:hover, .dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus { .dropdown-menu > li > a:focus {
color: #fff; color: #eee;
background-color: #2f2f2f; background-color: #2f2f2f;
} }
.dropdown-menu > .active > a, .dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus { .dropdown-menu > .active > a:focus {
color: #fff; color: #eee;
background-color: #2f2f2f; background-color: #2f2f2f;
} }
.dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a,
@@ -327,14 +327,14 @@ object {
background-color: #3B3B3B; background-color: #3B3B3B;
} }
.btn-dark:hover { .btn-dark:hover {
color: #fff; color: #eee;
background-color: #333; background-color: #333;
border-color: #444; border-color: #444;
} }
.btn-dark:active, .btn-dark:active,
.btn-dark.active, .btn-dark.active,
.open > .dropdown-toggle.btn-dark { .open > .dropdown-toggle.btn-dark {
color: #fff; color: #eee;
background-color: #333; background-color: #333;
border-color: #444; border-color: #444;
} }
@@ -347,7 +347,7 @@ object {
.btn-dark:active.focus, .btn-dark:active.focus,
.btn-dark.active.focus, .btn-dark.active.focus,
.open > .dropdown-toggle.btn-dark.focus { .open > .dropdown-toggle.btn-dark.focus {
color: #fff; color: #eee;
background-color: #333; background-color: #333;
} }
.btn-dark:active, .btn-dark:active,
@@ -387,24 +387,24 @@ fieldset[disabled] .btn-dark.active {
background-color: #3B3B3B; background-color: #3B3B3B;
} }
.btn-bright { .btn-bright {
color: #fff; color: #eee;
background-color: #cc7b19; background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b; box-shadow: inset 0 1px 0 #e7993b;
} }
.btn-bright:focus, .btn-bright:focus,
.btn-bright.focus { .btn-bright.focus {
color: #fff; color: #eee;
background-color: #eb8600; background-color: #eb8600;
} }
.btn-bright:hover { .btn-bright:hover {
color: #fff; color: #eee;
background-color: #e59029; background-color: #e59029;
box-shadow: inset 0 1px 0 #ebac60; box-shadow: inset 0 1px 0 #ebac60;
} }
.btn-bright:active, .btn-bright:active,
.btn-bright.active, .btn-bright.active,
.open > .dropdown-toggle.btn-bright { .open > .dropdown-toggle.btn-bright {
color: #fff; color: #eee;
background-color: #cc7b19; background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b; box-shadow: inset 0 1px 0 #e7993b;
} }
@@ -417,7 +417,7 @@ fieldset[disabled] .btn-dark.active {
.btn-bright:active.focus, .btn-bright:active.focus,
.btn-bright.active.focus, .btn-bright.active.focus,
.open > .dropdown-toggle.btn-bright.focus { .open > .dropdown-toggle.btn-bright.focus {
color: #fff; color: #eee;
background-color: #cc7b19; background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b; box-shadow: inset 0 1px 0 #e7993b;
} }
@@ -448,7 +448,7 @@ fieldset[disabled] .btn-bright.active {
border-color: #b56d16; border-color: #b56d16;
} }
.btn-bright .badge { .btn-bright .badge {
color: #fff; color: #eee;
background-color: #cc7b19; background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b; box-shadow: inset 0 1px 0 #e7993b;
} }
@@ -459,22 +459,26 @@ fieldset[disabled] .btn-bright.active {
float: right; float: right;
} }
.btn-danger.btn-edit:hover { .btn-danger.btn-edit:hover {
color: #fff; color: #eee;
background-color: #c9302c; background-color: #c9302c;
border-color: #ac2925; border-color: #ac2925;
} }
.btn-danger.btn-edit.active { .btn-danger.btn-edit.active {
color: #fff; color: #eee;
background-color: #c9302c; background-color: #c9302c;
border-color: #ac2925; border-color: #ac2925;
} }
.btn-danger.btn-edit.active:hover { .btn-danger.btn-edit.active:hover {
color: #fff; color: #eee;
background-color: #ac2925; background-color: #ac2925;
border-color: #761c19; border-color: #761c19;
} }
.btn-group select { .btn-group select {
margin-top: 0; margin-top: 0;
height: 34px;
}
.btn-group label {
margin-bottom: 0;
} }
.input-group-addon-form { .input-group-addon-form {
display: inline-block; display: inline-block;
@@ -488,9 +492,6 @@ fieldset[disabled] .btn-bright.active {
width: 100%; width: 100%;
margin-top: 5px; margin-top: 5px;
} }
#user-selection label {
margin-bottom: 0;
}
.alert-edit { .alert-edit {
display: none; display: none;
float: left; float: left;
@@ -512,7 +513,7 @@ fieldset[disabled] .btn-bright.active {
background-color: #222222; background-color: #222222;
} }
.modal-body table { .modal-body table {
color: #fff; color: #eee;
} }
.modal-body li { .modal-body li {
margin-top: 7px; margin-top: 7px;
@@ -523,10 +524,10 @@ fieldset[disabled] .btn-bright.active {
color: #eee; color: #eee;
} }
.modal-body i { .modal-body i {
color: #F9AA03; color: #E5A00D;
} }
.modal-body i.fa { .modal-body i.fa {
color: #fff; color: #eee;
} }
.modal-body td:hover a .fa, .modal-body td:hover a .fa,
.modal-body a:focus i.fa { .modal-body a:focus i.fa {
@@ -534,7 +535,7 @@ fieldset[disabled] .btn-bright.active {
} }
.modal-body strong, .modal-body strong,
.modal-body strong i.fa { .modal-body strong i.fa {
color: #F9AA03; color: #E5A00D;
} }
.modal-footer { .modal-footer {
padding: 15px 20px; padding: 15px 20px;
@@ -560,7 +561,7 @@ input[type="tel"],
input[type="color"], input[type="color"],
.uneditable-input { .uneditable-input {
margin: 5px 0 5px 0; margin: 5px 0 5px 0;
color: #fff; color: #eee;
border: 0px solid #444; border: 0px solid #444;
background: #555; background: #555;
height: 32px; height: 32px;
@@ -572,7 +573,7 @@ input[type="color"],
textarea.form-control { textarea.form-control {
height: initial; height: initial;
margin: 5px 0 5px 0; margin: 5px 0 5px 0;
color: #fff; color: #eee;
border: 0px solid #444; border: 0px solid #444;
background: #555; background: #555;
padding: 6px 12px; padding: 6px 12px;
@@ -584,7 +585,7 @@ textarea.form-control {
textarea.form-control:focus { textarea.form-control:focus {
outline: 0; outline: 0;
color: #555; color: #555;
background-color: #fff; background-color: #eee;
transition: background-color .3s; transition: background-color .3s;
} }
.pagination > li > a, .pagination > li > a,
@@ -594,7 +595,7 @@ textarea.form-control:focus {
padding: 6px 12px; padding: 6px 12px;
margin-left: -1px; margin-left: -1px;
line-height: 1.42857143; line-height: 1.42857143;
color: #fff; color: #eee;
text-decoration: none; text-decoration: none;
background-color: #262626; background-color: #262626;
border: 1px solid #444444; border: 1px solid #444444;
@@ -613,7 +614,7 @@ textarea.form-control:focus {
.pagination > .active > a:focus, .pagination > .active > a:focus,
.pagination > .active > span:focus { .pagination > .active > span:focus {
z-index: 2; z-index: 2;
color: #fff; color: #eee;
cursor: default; cursor: default;
background-color: #cc7b19; background-color: #cc7b19;
border-color: #444444; border-color: #444444;
@@ -632,7 +633,7 @@ textarea.form-control:focus {
.nav-pills > li.active > a, .nav-pills > li.active > a,
.nav-pills > li.active > a:hover, .nav-pills > li.active > a:hover,
.nav-pills > li.active > a:focus { .nav-pills > li.active > a:focus {
color: #fff; color: #eee;
background-color: #cc7b19; background-color: #cc7b19;
} }
.nav-pills > li > a { .nav-pills > li > a {
@@ -666,14 +667,14 @@ textarea.form-control:focus {
-webkit-appearance:none; -webkit-appearance:none;
} }
.btn-form:hover { .btn-form:hover {
color: #fff; color: #eee;
background-color: #333; background-color: #333;
} }
.btn-form:focus { .btn-form:focus {
color: #fff; color: #eee;
} }
.form-control-feedback { .form-control-feedback {
color: #F9AA03; color: #E5A00D;
margin: 5px 40px 5px 0; margin: 5px 40px 5px 0;
} }
.form-control[disabled], .form-control[disabled],
@@ -682,7 +683,7 @@ fieldset[disabled] .form-control {
background-color: #555; background-color: #555;
} }
.form-control[readonly]:focus { .form-control[readonly]:focus {
background-color: #fff; background-color: #eee;
} }
.poster { .poster {
position: relative; position: relative;
@@ -711,7 +712,6 @@ fieldset[disabled] .form-control {
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
} }
.users-poster-face { .users-poster-face {
overflow: hidden;
float: left; float: left;
background-size: cover; background-size: cover;
background-position: center; background-position: center;
@@ -857,7 +857,6 @@ a .users-poster-face:hover {
z-index: 2; z-index: 2;
} }
.dashboard-activity-info-platform { .dashboard-activity-info-platform {
padding: 6px !important;
background-position: center; background-position: center;
background-size: cover; background-size: cover;
width: 50px; width: 50px;
@@ -973,7 +972,7 @@ a .users-poster-face:hover {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 140px; max-width: 125px;
} }
.dashboard-activity-info-time { .dashboard-activity-info-time {
position: absolute; position: absolute;
@@ -1032,17 +1031,17 @@ a .users-poster-face:hover {
height: 249px; height: 249px;
} }
.dashboard-activity-container:hover .dashboard-activity-progress { .dashboard-activity-container:hover .dashboard-activity-progress {
height: 14px; height: 14px;
} }
.dashboard-activity-container:hover .progress-bar { .dashboard-activity-container:hover .progress-bar {
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25),0%,rgba(0,0,0,0),50px); background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px); background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px); background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
} }
.dashboard-activity-container:hover .buffer-bar { .dashboard-activity-container:hover .buffer-bar {
color: rgba(255, 255, 255, 1); color: rgba(255, 255, 255, 1);
background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25),0%,rgba(0,0,0,0),50px); background-image: -webkit-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px); background-image: -moz-linear-gradient(left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px); background-image: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
} }
@@ -1073,7 +1072,7 @@ a:hover .dashboard-activity-cover {
font-size: 13px; font-size: 13px;
font-weight: bold; font-weight: bold;
line-height: 25px; line-height: 25px;
color: #fff; color: #eee;
} }
.dashboard-activity-metadata-play_state-icon { .dashboard-activity-metadata-play_state-icon {
flex-basis: 25px; flex-basis: 25px;
@@ -1536,7 +1535,7 @@ a:hover .dashboard-recent-media-cover {
} }
.dashboard-recent-media-metacontainer h3 { .dashboard-recent-media-metacontainer h3 {
padding: 5px 3px 0 3px; padding: 5px 3px 0 3px;
color: #fff; color: #eee;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -1649,12 +1648,12 @@ a:hover .dashboard-recent-media-cover {
color: #f9be03; color: #f9be03;
} }
.summary-content-title h1 a:hover { .summary-content-title h1 a:hover {
color: #fff; color: #eee;
} }
.summary-content-title h2 { .summary-content-title h2 {
margin-top: 0; margin-top: 0;
margin-bottom: 10px; margin-bottom: 10px;
color: #fff; color: #eee;
font-size: 28px; font-size: 28px;
line-height: 40px; line-height: 40px;
float: left; float: left;
@@ -1742,7 +1741,7 @@ a:hover .dashboard-recent-media-cover {
top: 0; top: 0;
bottom: 0; bottom: 0;
background-image: -webkit-gradient(linear,left 0,left 100%,from(rgba(0,0,0,.7)),to(rgba(0,0,0,.9))); background-image: -webkit-gradient(linear,left 0,left 100%,from(rgba(0,0,0,.7)),to(rgba(0,0,0,.9)));
background-image: -webkit-linear-gradient(top,rgba(0,0,0,.7),0,rgba(0,0,0,.9),100%); background-image: -webkit-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%); background-image: -moz-linear-gradient(top,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%); background-image: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
background-repeat: repeat-x; background-repeat: repeat-x;
@@ -1808,7 +1807,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
line-height: 24px; line-height: 24px;
} }
.summary-content-details-tag strong { .summary-content-details-tag strong {
color: #fff; color: #eee;
margin-left: 2px; margin-left: 2px;
margin-right: 10px; margin-right: 10px;
} }
@@ -1828,7 +1827,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
} }
.summary-content-summary { .summary-content-summary {
overflow: hidden; overflow: hidden;
color: #fff; color: #eee;
float: left; float: left;
position: relative; position: relative;
clear: both; clear: both;
@@ -1862,7 +1861,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
display: block; display: block;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: #fff; color: #eee;
} }
.summary-content-genres { .summary-content-genres {
margin-top: 13px; margin-top: 13px;
@@ -1881,7 +1880,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
display: block; display: block;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: #fff; color: #eee;
} }
.summary-content-writers { .summary-content-writers {
margin-top: 13px; margin-top: 13px;
@@ -1900,7 +1899,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
display: block; display: block;
font-size: 12px; font-size: 12px;
line-height: 18px; line-height: 18px;
color: #fff; color: #eee;
} }
.star-rating { .star-rating {
display: inline-block; display: inline-block;
@@ -1921,6 +1920,16 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
margin-left: 2px; margin-left: 2px;
color: #999; color: #999;
} }
.critic-rating {
display: inline-block;
font-size: 14px;
overflow: hidden;
white-space: nowrap;
margin-top: 2px;
height: 20px;
line-height: 20px;
float: right;
}
.children-list, .children-list,
.search-results-list { .search-results-list {
position: relative; position: relative;
@@ -1943,7 +1952,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
position: relative; position: relative;
margin: 0; margin: 0;
line-height: 22px; line-height: 22px;
color: #fff; color: #eee;
font-size: 16px; font-size: 16px;
text-align: center; text-align: center;
text-transform: uppercase; text-transform: uppercase;
@@ -2039,7 +2048,7 @@ a:hover .item-children-poster {
.item-children-instance-text-wrapper h3 { .item-children-instance-text-wrapper h3 {
width: 100%; width: 100%;
padding: 5px 3px 0 3px; padding: 5px 3px 0 3px;
color: #fff; color: #eee;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -2104,7 +2113,7 @@ a:hover .item-children-poster {
margin-right: 20px; margin-right: 20px;
} }
#new_title h3 { #new_title h3 {
color: #f9be03; color: #E5A00D;
font-size: 14px; font-size: 14px;
line-height: 1.42857143; line-height: 1.42857143;
font-weight: bold; font-weight: bold;
@@ -2140,7 +2149,7 @@ span.settings-warning {
padding-left: 10px; padding-left: 10px;
} }
#menu_link_show_advanced_settings.active { #menu_link_show_advanced_settings.active {
color: #fff; color: #eee;
background-color: #cc7b19; background-color: #cc7b19;
} }
.advanced-setting { .advanced-setting {
@@ -2153,7 +2162,7 @@ div.advanced-setting {
li.advanced-setting { li.advanced-setting {
border-left: 1px solid #cc7b19; border-left: 1px solid #cc7b19;
} }
.docker-setting { .setting-message {
color: #cc7b19; color: #cc7b19;
margin-left: 10px; margin-left: 10px;
} }
@@ -2175,9 +2184,9 @@ li.advanced-setting {
} }
.user-info-username { .user-info-username {
font-size: 24px; font-size: 24px;
color: #fff; color: #eee;
padding-top: 27px; padding-top: 27px;
padding-left: 110px; padding-left: 105px;
} }
.user-info-nav { .user-info-nav {
margin-top: 15px; margin-top: 15px;
@@ -2241,7 +2250,7 @@ li.advanced-setting {
left: 0px; left: 0px;
} }
.user-overview-stats-instance h3 strong{ .user-overview-stats-instance h3 strong{
color: #fff; color: #eee;
} }
.user-overview-stats-instance h3 { .user-overview-stats-instance h3 {
font-size: 30px; font-size: 30px;
@@ -2254,7 +2263,7 @@ li.advanced-setting {
float: left; float: left;
} }
.user-overview-stats-instance h4 { .user-overview-stats-instance h4 {
color: #fff; color: #eee;
margin-bottom: 25px; margin-bottom: 25px;
} }
.user-overview-stats-instance h1 { .user-overview-stats-instance h1 {
@@ -2294,7 +2303,7 @@ li.advanced-setting {
.user-player-instance-name { .user-player-instance-name {
float: left; float: left;
padding-top: 14px; padding-top: 14px;
color: #fff; color: #eee;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -2304,6 +2313,7 @@ li.advanced-setting {
width: 140px; width: 140px;
margin-left: 10px; margin-left: 10px;
margin-bottom: 10px; margin-bottom: 10px;
white-space: nowrap;
} }
.user-player-instance-playcount h3 { .user-player-instance-playcount h3 {
font-size: 30px; font-size: 30px;
@@ -2352,9 +2362,6 @@ a .library-user-instance-box:hover {
-moz-box-shadow: inset 0 0 0 2px #e9a049; -moz-box-shadow: inset 0 0 0 2px #e9a049;
box-shadow: inset 0 0 0 2px #e9a049; box-shadow: inset 0 0 0 2px #e9a049;
} }
#watched-stats-days-selection label {
margin-bottom: 0;
}
.home-padded-header { .home-padded-header {
margin: 25px 0; margin: 25px 0;
height: 34px; height: 34px;
@@ -2432,7 +2439,7 @@ a .library-user-instance-box:hover {
overflow: hidden; overflow: hidden;
} }
.home-platforms-instance-name { .home-platforms-instance-name {
color: #fff; color: #eee;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -2619,7 +2626,7 @@ a .library-user-instance-box:hover {
} }
.home-platforms-instance-list-name { .home-platforms-instance-list-name {
float: left; float: left;
color: #fff; color: #eee;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -2986,6 +2993,9 @@ a .home-platforms-list-cover-face:hover
.accordion li .link i.fa { .accordion li .link i.fa {
color: #999; color: #999;
} }
.accordion li .link span.toggle-left {
padding-right: 5px;
}
.accordion li .link span.toggle-right { .accordion li .link span.toggle-right {
float: right; float: right;
padding-left: 10px; padding-left: 10px;
@@ -3031,7 +3041,7 @@ a .home-platforms-list-cover-face:hover
} }
.submenu a:hover { .submenu a:hover {
background: #f9be03; background: #f9be03;
color: #FFF; color: #eee;
} }
.ajaxMsg { .ajaxMsg {
background-color: rgba(255,255,255,0.075); background-color: rgba(255,255,255,0.075);
@@ -3090,24 +3100,40 @@ div.dataTables_info {
white-space: normal !important; white-space: normal !important;
} }
.tooltip.top .tooltip-arrow { .tooltip.top .tooltip-arrow {
border-top-color: #fff; border-top-color: #eee;
} }
.tooltip.right .tooltip-arrow { .tooltip.right .tooltip-arrow {
border-right-color: #fff; border-right-color: #eee;
} }
.tooltip.bottom .tooltip-arrow { .tooltip.bottom .tooltip-arrow {
border-bottom-color: #fff; border-bottom-color: #eee;
} }
.tooltip.left .tooltip-arrow { .tooltip.left .tooltip-arrow {
border-left-color: #fff; border-left-color: #eee;
} }
.tooltip-inner { .tooltip-inner {
max-width: 250px;
color: #000; color: #000;
background: #fff; background: #eee;
border: 0; border: 0;
font-weight: bold; font-weight: bold;
border-radius: 2px; border-radius: 2px;
} }
.inactive-library-tooltip,
.inactive-user-tooltip {
display: inline-block;
position: relative;
width: 100%;
height: 100%;
}
.inactive-library-tooltip i.fa,
.inactive-user-tooltip i.fa {
color: #E5A00D;
position: absolute;
right: 0;
bottom: 0;
text-shadow: 0 0 2px rgba(0,0,0,.5);
}
.history-thumbnail-popover { .history-thumbnail-popover {
z-index: 2000; z-index: 2000;
padding: 0; padding: 0;
@@ -3134,6 +3160,37 @@ div.dataTables_info {
-moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); -moz-box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1); box-shadow: 0 0 4px rgba(0,0,0,.3),inset 0 0 0 1px rgba(255,255,255,.1);
} }
.channel-thumbnail-popover {
z-index: 2000;
padding: 0;
border: 0;
}
.channel-thumbnail-popover.popover.left {
margin-left: -15px;
}
.channel-thumbnail-popover.popover.right {
margin-left: 15px;
}
.channel-thumbnail-popover .popover-content {
color: #000;
padding: 0;
}
.channel-thumbnail {
background-color: #868b8b;
background-position: center;
background-size: contain;
background-origin: content-box;
background-repeat: no-repeat;
height: 50px;
width: 50px;
padding: 3px;
-webkit-border-radius: 3px;
-moz-border-radius: 3px;
border-radius: 3px;
}
.channel-thumbnail-popover .arrow:after {
border-right-color: #868b8b !important;
}
.edit-user-toggles, .edit-user-toggles,
.edit-library-toggles { .edit-library-toggles {
padding-right: 10px; padding-right: 10px;
@@ -3149,7 +3206,7 @@ div.dataTables_info {
} }
.edit-user-toggles > input[type='checkbox']:checked + label, .edit-user-toggles > input[type='checkbox']:checked + label,
.edit-library-toggles > input[type='checkbox']:checked + label { .edit-library-toggles > input[type='checkbox']:checked + label {
color: #fff; color: #eee;
cursor: pointer; cursor: pointer;
} }
.edit-user-name > input[type='text'] { .edit-user-name > input[type='text'] {
@@ -3367,9 +3424,33 @@ pre::-webkit-scrollbar-thumb {
.notification-params tr:nth-child(even) td { .notification-params tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010); background-color: rgba(255,255,255,0.010);
} }
#days-selection label, .activity-queue {
#months-selection label { width: 100%;
margin-bottom: 0; margin-top: 10px;
background-color: #282828;
}
.activity-queue th {
padding-left: 10px;
height: 30px;
}
.activity-queue th:first-child {
width: 268px;
}
.activity-queue th:nth-child(2) {
width: 125px;
}
.activity-queue th:nth-child(3) {
width: 175px;
}
.activity-queue td {
height: 25px;
padding: 5px 10px;
}
.activity-queue tr:nth-child(odd) td {
background-color: rgba(255,255,255,0.035);
}
.activity-queue tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010);
} }
.card-sortable { .card-sortable {
height: 36px; height: 36px;
@@ -3425,13 +3506,13 @@ pre::-webkit-scrollbar-thumb {
width: 225px; width: 225px;
} }
.config-scheduler-table th { .config-scheduler-table th {
color: #fff; color: #eee;
} }
a.no-highlight { a.no-highlight {
color: #777; color: #777;
} }
a.no-highlight:hover { a.no-highlight:hover {
color: #fff; color: #eee;
} }
.top-line { .top-line {
border-top: 1px dotted #777; border-top: 1px dotted #777;
@@ -3439,7 +3520,7 @@ a.no-highlight:hover {
} }
.help-bold { .help-bold {
font-weight: bold; font-weight: bold;
color: #fff; color: #eee;
} }
.save-button { .save-button {
margin-top: 15px; margin-top: 15px;
@@ -3584,7 +3665,7 @@ a.no-highlight:hover {
margin: 0 2px; margin: 0 2px;
padding: 2px 5px; padding: 2px 5px;
font-size: 13px; font-size: 13px;
color: #fff; color: #eee;
background-color: #555; background-color: #555;
border: 0px solid #444; border: 0px solid #444;
border-radius: 3px; border-radius: 3px;
@@ -3602,7 +3683,7 @@ a.no-highlight:hover {
-webkit-transition: all .1s cubic-bezier(.4,0,1,1); -webkit-transition: all .1s cubic-bezier(.4,0,1,1);
-moz-transition: all .1s cubic-bezier(.4,0,1,1); -moz-transition: all .1s cubic-bezier(.4,0,1,1);
-o-transition: all .1s cubic-bezier(.4,0,1,1); -o-transition: all .1s cubic-bezier(.4,0,1,1);
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff, -1px 1px 0 #fff, 1px 1px 0 #fff; text-shadow: -1px -1px 0 #eee, 1px -1px 0 #eee, -1px 1px 0 #eee, 1px 1px 0 #eee;
} }
.overlay-refresh-image.left { .overlay-refresh-image.left {
left: 10px; left: 10px;
@@ -3616,7 +3697,7 @@ a.no-highlight:hover {
cursor: pointer; cursor: pointer;
} }
.overlay-refresh-image.info-art:hover { .overlay-refresh-image.info-art:hover {
color: #fff; color: #eee;
text-shadow: none; text-shadow: none;
} }
a:hover .overlay-refresh-image { a:hover .overlay-refresh-image {
@@ -3633,10 +3714,6 @@ a:hover .overlay-refresh-image:hover {
padding-top: 10px; padding-top: 10px;
padding-bottom: 10px; padding-bottom: 10px;
} }
#plexpy-log-levels label,
#plex-log-levels label {
margin-bottom: 0;
}
#plexpy-notifiers-table .friendly_name, #plexpy-notifiers-table .friendly_name,
#notifier-config-modal span.notifier_id, #notifier-config-modal span.notifier_id,
#plexpy-newsletters-table .friendly_name, #plexpy-newsletters-table .friendly_name,
@@ -3671,7 +3748,7 @@ a:hover .overlay-refresh-image:hover {
#newsletter-config-modal .nav-tabs > li.active > a, #newsletter-config-modal .nav-tabs > li.active > a,
#newsletter-config-modal .nav-tabs > li.active > a:hover, #newsletter-config-modal .nav-tabs > li.active > a:hover,
#newsletter-config-modal .nav-tabs > li.active > a:focus { #newsletter-config-modal .nav-tabs > li.active > a:focus {
color: #fff; color: #eee;
background: #222; background: #222;
} }
#notifier-config-modal .nav-tabs > li.active > a, #notifier-config-modal .nav-tabs > li.active > a,
@@ -3738,9 +3815,8 @@ a:hover .overlay-refresh-image:hover {
} }
.svg-icon { .svg-icon {
padding: 10px; background-size: calc(100% - 20px) calc(100% - 20px) !important;
background-origin: content-box !important; background-origin: content-box !important;
background-size: contain !important;
background-repeat: no-repeat !important; background-repeat: no-repeat !important;
background-position: center !important; background-position: center !important;
} }
@@ -3788,6 +3864,10 @@ a:hover .overlay-refresh-image:hover {
background-color: #31afe1; background-color: #31afe1;
background-image: url(../images/platforms/kodi.svg); background-image: url(../images/platforms/kodi.svg);
} }
.platform-lg {
background-color: #a50034;
background-image: url(../images/platforms/lg.svg);
}
.platform-linux { .platform-linux {
background-color: #1793d0; background-color: #1793d0;
background-image: url(../images/platforms/linux.svg); background-image: url(../images/platforms/linux.svg);
@@ -3850,7 +3930,7 @@ a:hover .overlay-refresh-image:hover {
} }
.platform-xbmc { .platform-xbmc {
background-color: #3b4872; background-color: #3b4872;
background-image: url(../images/platforms/xbmc.svg); background-image: url(../images/platforms/kodi.svg);
} }
.platform-xbox { .platform-xbox {
background-color: #107c10; background-color: #107c10;
@@ -3889,6 +3969,9 @@ a:hover .overlay-refresh-image:hover {
.platform-kodi-rgba { .platform-kodi-rgba {
background-color: rgba(49, 175, 225, 0.40); background-color: rgba(49, 175, 225, 0.40);
} }
.platform-lg-rgba {
background-color: rgba(165, 0, 52, 0.40);
}
.platform-linux-rgba { .platform-linux-rgba {
background-color: rgba(23, 147, 208, 0.40); background-color: rgba(23, 147, 208, 0.40);
} }
@@ -3955,10 +4038,44 @@ a:hover .overlay-refresh-image:hover {
.library-video { .library-video {
background-image: url(../images/libraries/video.svg); background-image: url(../images/libraries/video.svg);
} }
.library-live {
background-image: url(../images/libraries/live.svg);
}
.stats-most_concurrent { .stats-most_concurrent {
background-image: url(../images/icons/most-concurrent-streams.svg); background-image: url(../images/icons/most-concurrent-streams.svg);
} }
.rating-image {
width: 51px;
height: 20px;
margin-left: 10px;
display: inline-block;
background-origin: content-box !important;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: left !important;
text-align: right;
}
.rating-imdb {
width: 62px !important;
background-image: url(../images/rating/imdb.svg);
}
.rating-themoviedb {
width: 72px !important;
background-image: url(../images/rating/themoviedb.svg);
background-size: auto 16px !important;
}
.rating-rottentomatos-ripe {
background-image: url(../images/rating/tomato-ripe.svg);
}
.rating-rottentomatos-rotten {
background-image: url(../images/rating/tomato-rotten.svg);
}
.rating-rottentomatos-upright {
background-image: url(../images/rating/popcorn-upright.svg);
}
.rating-rottentomatos-spilled {
background-image: url(../images/rating/popcorn-spilled.svg);
}
.transparent { .transparent {
background-color: transparent !important; background-color: transparent !important;
} }
@@ -3990,7 +4107,7 @@ a:hover .overlay-refresh-image:hover {
flex-shrink: 0; flex-shrink: 0;
} }
#info-modal .stream-info-item .sub-value { #info-modal .stream-info-item .sub-value {
color: #fff; color: #eee;
font-weight: bold; font-weight: bold;
margin-left: 10px; margin-left: 10px;
text-align: left; text-align: left;
@@ -4005,7 +4122,7 @@ a:hover .overlay-refresh-image:hover {
table-layout: fixed; table-layout: fixed;
} }
.stream-info .heading { .stream-info .heading {
color: #F9AA03; color: #E5A00D;
text-transform: uppercase; text-transform: uppercase;
font-size: 15px; font-size: 15px;
font-weight: bold !important; font-weight: bold !important;
@@ -4013,7 +4130,7 @@ a:hover .overlay-refresh-image:hover {
.stream-info th:first-child { .stream-info th:first-child {
width: 125px; width: 125px;
height: 30px; height: 30px;
color: #fff; color: #eee;
font-size: 12px; font-size: 12px;
text-align: right; text-align: right;
text-transform: uppercase; text-transform: uppercase;
@@ -4136,7 +4253,7 @@ a[data-tab-destination] {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.iframe-button { .iframe-button {
color: #fff; color: #eee;
border-radius: 20px; border-radius: 20px;
text-align: center; text-align: center;
cursor: pointer; cursor: pointer;
@@ -4153,7 +4270,7 @@ a[data-tab-destination] {
} }
.iframe-button:hover, .iframe-button:hover,
.iframe-button:focus { .iframe-button:focus {
color: #fff; color: #eee;
box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 99999px inset, rgba(0, 0, 0, 0.2) 0px 1px 5px 0px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 3px 1px -2px; box-shadow: rgba(0, 0, 0, 0.1) 0px 0px 0px 99999px inset, rgba(0, 0, 0, 0.2) 0px 1px 5px 0px, rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 3px 1px -2px;
} }
.iframe-button:active { .iframe-button:active {
@@ -4176,8 +4293,50 @@ a[data-tab-destination] {
background: #cc7b19; background: #cc7b19;
text-align: center; text-align: center;
font-weight: bold; font-weight: bold;
padding-top: 2px; padding: 2px 10px;
position: absolute; position: absolute;
top: 0; top: 0;
z-index: 9999; z-index: 9999;
} }
.help-block li {
margin-top: 0;
color: #737373;
}
#browse-path-list > li > span > i.fa {
color: #999;
}
#tautulli-news .open .news-title,
#tautulli-news .open .news-date,
#tautulli-news .accordion li.open .link i.fa {
color: #eee;
}
.news-title,
.news-date {
color: #999;
padding-left: 5px;
}
.news-subtitle {
display: block;
color: #aaa;
font-weight: bold;
margin-bottom: 10px;
}
.news-body {
display: block;
color: #aaa;
}
.news-body p:last-of-type {
margin-bottom: 0;
}
.news-body a {
display: inline !important;
background: none !important;
padding: 0 !important;
color: #eee;
}
.news-body a:hover {
color: #f9be03;
}

View File

@@ -62,8 +62,7 @@ DOCUMENTATION :: END
% if session is not None: % if session is not None:
<% <%
from collections import defaultdict from collections import defaultdict
from urllib import quote from plexpy.helpers import cast_to_int, page
from plexpy import helpers
from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES from plexpy.common import VIDEO_RESOLUTION_OVERRIDES, AUDIO_CODEC_OVERRIDES, EXTRA_TYPES
import plexpy import plexpy
%> %>
@@ -71,58 +70,65 @@ DOCUMENTATION :: END
data = defaultdict(lambda: 'Unknown', **session) data = defaultdict(lambda: 'Unknown', **session)
sk = data['session_key'] sk = data['session_key']
href = 'info?rating_key={}'.format(data['rating_key']) if data['rating_key'] else '#' href = page('info', data['rating_key'])
parent_href = 'info?rating_key={}'.format(data['parent_rating_key']) if data['parent_rating_key'] else '#' parent_href = page('info', data['parent_rating_key'])
grandparent_href = 'info?rating_key={}'.format(data['grandparent_rating_key']) if data['grandparent_rating_key'] else '#' grandparent_href = page('info', data['grandparent_rating_key'])
user_href = 'user?user_id={}'.format(data['user_id']) if data['user_id'] else '#' user_href = page('user', 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']}" <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']}"> data-rating_key="${data['rating_key']}" data-parent_rating_key="${data['parent_rating_key']}" data-grandparent_rating_key="${data['grandparent_rating_key']}"
data-guid="${data['guid']}">
<div class="dashboard-activity-container"> <div class="dashboard-activity-container">
<% <%
if data['channel_stream'] == 0: if data['live']:
background_url = 'pms_image_proxy?img=' + data['art'] + '&width=500&height=280&opacity=40&background=282828&blur=3&fallback=art&refresh=true' background_url = page('pms_image_proxy', data['art'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art-live', refresh=True)
elif data['channel_stream'] == 0:
background_url = page('pms_image_proxy', data['art'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art', refresh=True)
else: else:
if (data['art'] and data['art'].startswith('http')) or (data['thumb'] and data['thumb'].startswith('http')): background_url = page('pms_image_proxy', data['art'] or data['thumb'], data['rating_key'], 500, 280, 40, '282828', 3, fallback='art', refresh=True, clip=True)
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 id="background-${sk}" class="dashboard-activity-background" style="background-image: url(${background_url});">
<div class="dashboard-activity-poster-container hidden-xs"> <div class="dashboard-activity-poster-container hidden-xs">
% if data['media_type'] == 'track': % if data['media_type'] == 'track':
<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> <div id="poster-${sk}-bg" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 300, 60, '282828', 3, fallback='cover', refresh=True)});"></div>
% endif % endif
% if data['channel_stream'] == 0: % if data['live']:
% if data['media_type'] == 'movie': % if data['media_type'] == 'movie':
<a id="poster-url-${sk}" href="${href}" 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> <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster-live', refresh=True)});"></div>
</a>
% elif data['media_type'] == 'episode':
<a id="poster-url-${sk}" href="${href}" title="${data['grandparent_title']}">
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'], data['rating_key'], 300, 450, fallback='poster-live', refresh=True)});"></div>
</a>
% else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live', refresh=True)});"></div>
% endif
% elif data['channel_stream'] == 0:
% if data['media_type'] == 'movie':
<a id="poster-url-${sk}" href="${href}" title="${data['title']}">
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
</a> </a>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<a id="poster-url-${sk}" href="${grandparent_href}" 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> <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'], data['grandparent_rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
</a> </a>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<a id="poster-url-${sk}" href="${parent_href}" 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> <div id="poster-${sk}" class="dashboard-activity-cover" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 300, fallback='cover', refresh=True)});"></div>
</a> </a>
% elif data['media_type'] in ('photo', 'clip'): % elif data['media_type'] in ('photo', 'clip'):
% if data['extra_type']: % if data['parent_thumb']:
<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> <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['parent_thumb'], data['parent_rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
% else: % 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> <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
% endif % endif
% else: % else:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/art.png);"></div> <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(images/poster.png);"></div>
% endif % endif
% else: % else:
% if data['channel_icon'].startswith('http'): <div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['channel_icon'], data['rating_key'], 300, 300, 60, '282828', 3, fallback='cover', refresh=True)});"></div>
<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(${page('pms_image_proxy', data['channel_icon'], data['rating_key'], 300, 300, fallback='cover', refresh=True)});"></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" 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 % endif
</div> </div>
<div class="dashboard-activity-info-icon"> <div class="dashboard-activity-info-icon">
@@ -135,7 +141,7 @@ DOCUMENTATION :: END
<div id="platform-${sk}" class="dashboard-activity-info-platform${no_terminate} svg-icon platform-${data['platform_name']}" title="${data['platform']}"></div> <div id="platform-${sk}" class="dashboard-activity-info-platform${no_terminate} svg-icon platform-${data['platform_name']}" title="${data['platform']}"></div>
% if _session['user_group'] == 'admin' and plexpy.CONFIG.PMS_PLEXPASS and data['session_id']: % if _session['user_group'] == 'admin' and plexpy.CONFIG.PMS_PLEXPASS and data['session_id']:
<div class="dashboard-activity-terminate-session" id="terminate-button-${sk}" data-key="${sk}" data-id="${data['session_id']}" data-toggle="tooltip" title="Terminate Stream"> <div class="dashboard-activity-terminate-session" id="terminate-button-${sk}" data-key="${sk}" data-id="${data['session_id']}" data-toggle="tooltip" title="Terminate Stream">
<i class="fa fa-times" style="padding-top: 8px;"></i> <i class="fa fa-times" style="padding-top: 10px;"></i>
</div> </div>
% endif % endif
</div> </div>
@@ -156,7 +162,7 @@ DOCUMENTATION :: END
<div class="sub-value platform-right" id="stream_quality-${sk}"> <div class="sub-value platform-right" id="stream_quality-${sk}">
% if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown': % if data['media_type'] != 'photo' and data['quality_profile'] != 'Unknown':
<% <%
br = helpers.cast_to_int(data['stream_bitrate']) or '' br = cast_to_int(data['stream_bitrate']) or ''
if br: if br:
if br > 1000: if br > 1000:
br = '(' + str(round(br / 1000.0, 1)) + ' Mbps)' br = '(' + str(round(br / 1000.0, 1)) + ' Mbps)'
@@ -212,9 +218,9 @@ DOCUMENTATION :: END
<div class="sub-heading">Container</div> <div class="sub-heading">Container</div>
<div class="sub-value" id="transcode_container-${sk}"> <div class="sub-value" id="transcode_container-${sk}">
% if data['stream_container_decision'] == 'transcode': % if data['stream_container_decision'] == 'transcode':
Transcode (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()}) Converting (${data['container'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_container'].upper()})
% else: % else:
Direct Play (${data['container'].upper()}) Direct Play (${data['stream_container'].upper()})
% endif % endif
</div> </div>
</li> </li>
@@ -222,17 +228,24 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Video</div> <div class="sub-heading">Video</div>
<div class="sub-value" id="video_decision-${sk}"> <div class="sub-value" id="video_decision-${sk}">
% if data['media_type'] in ('movie', 'episode', 'clip'): % if data['media_type'] in ('movie', 'episode', 'clip') and data['stream_video_decision']:
<%
if data['video_dynamic_range'] == 'HDR':
video_dynamic_range = ' ' + data['video_dynamic_range']
stream_video_dynamic_range = ' ' + data['stream_video_dynamic_range']
else:
video_dynamic_range = stream_video_dynamic_range = ''
%>
% if data['stream_video_decision'] == 'transcode': % if data['stream_video_decision'] == 'transcode':
<% <%
hw_d = ' (HW)' if data['transcode_hw_decoding'] else '' hw_d = ' (HW)' if data['transcode_hw_decoding'] else ''
hw_e = ' (HW)' if data['transcode_hw_encoding'] else '' hw_e = ' (HW)' if data['transcode_hw_encoding'] else ''
%> %>
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'])}) Transcode (${data['video_codec'].upper()}${hw_d} ${data['video_full_resolution']}${video_dynamic_range} <i class="fa fa-long-arrow-right"></i> ${data['stream_video_codec'].upper()}${hw_e} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
% elif data['stream_video_decision'] == 'copy': % 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'])}) Direct Stream (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
% else: % else:
Direct Play (${data['video_codec'].upper()} ${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])}) Direct Play (${data['stream_video_codec'].upper()} ${data['stream_video_full_resolution']}${stream_video_dynamic_range})
% endif % endif
% elif data['media_type'] == 'photo': % elif data['media_type'] == 'photo':
Direct Play (${data['width']}x${data['height']}) Direct Play (${data['width']}x${data['height']})
@@ -244,12 +257,14 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Audio</div> <div class="sub-heading">Audio</div>
<div class="sub-value" id="audio_decision-${sk}"> <div class="sub-value" id="audio_decision-${sk}">
% if data['stream_audio_decision'] == 'transcode': % if data['stream_audio_decision']:
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()}) % if data['stream_audio_decision'] == 'transcode':
% elif data['stream_audio_decision'] == 'copy': 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()})
Direct Stream (${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':
% else: Direct Stream (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_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()}) % else:
Direct Play (${AUDIO_CODEC_OVERRIDES.get(data['stream_audio_codec'], data['stream_audio_codec'].upper())} ${data['stream_audio_channel_layout'].split('(')[0].capitalize()})
% endif
% endif % endif
</div> </div>
</li> </li>
@@ -259,14 +274,17 @@ DOCUMENTATION :: END
<div class="sub-heading">Subtitle</div> <div class="sub-heading">Subtitle</div>
<div class="sub-value" id="subtitle_decision-${sk}"> <div class="sub-value" id="subtitle_decision-${sk}">
% if data['subtitles'] == 1: % if data['subtitles'] == 1:
<%
subtitle_codec = 'None' if data['stream_subtitle_codec'] and data['stream_subtitle_transient'] else data['subtitle_codec'].upper()
%>
% if data['stream_subtitle_decision'] == 'transcode': % if data['stream_subtitle_decision'] == 'transcode':
Transcode (${data['subtitle_codec'].upper()} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()}) Transcode (${subtitle_codec} <i class="fa fa-long-arrow-right"></i> ${data['stream_subtitle_codec'].upper()})
% elif data['stream_subtitle_decision'] == 'copy': % elif data['stream_subtitle_decision'] == 'copy':
Direct Stream (${data['subtitle_codec'].upper()}) Direct Stream (${subtitle_codec})
% elif data['stream_subtitle_decision'] == 'burn': % elif data['stream_subtitle_decision'] == 'burn':
Burn (${data['subtitle_codec'].upper()}) Burn (${subtitle_codec})
% else: % else:
Direct Play (${data['stream_subtitle_codec'].upper() if data['synced_version'] else data['subtitle_codec'].upper()}) Direct Play (${subtitle_codec if data['synced_version'] else data['stream_subtitle_codec'].upper()})
% endif % endif
% else: % else:
None None
@@ -279,10 +297,21 @@ DOCUMENTATION :: END
<li class="dashboard-activity-info-item"> <li class="dashboard-activity-info-item">
<div class="sub-heading">Location</div> <div class="sub-heading">Location</div>
<div class="sub-value time-right"> <div class="sub-value time-right">
% if data['secure'] is not None:
% if data['secure']:
<span data-toggle="tooltip" title="Secure Connection"><i class="fa fa-lock"></i></span>
% else:
<span data-toggle="tooltip" title="Insecure Connection"><i class="fa fa-unlock"></i></span>
% endif
% endif
<span id="location-${sk}">${data['location'].upper()}</span>: <span id="location-${sk}">${data['location'].upper()}</span>:
% if data['ip_address'] != 'N/A': % if data['ip_address'] != 'N/A':
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span> % if len(data['ip_address']) > 20:
% if data['relay']: <span class="ip-container"><span class="ip-address" data-toggle="tooltip" title="${data['ip_address']}">${data['ip_address']}</span></span>
% else:
<span class="ip-container"><span class="ip-address">${data['ip_address']}</span></span>
% endif
% if data['relayed']:
<span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span> <span data-toggle="tooltip" title="Plex Relay"><i class="fa fa-exclamation-circle"></i></span>
% else: % else:
<a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}"> <a href="#" class="external_ip-modal" data-toggle="modal" data-target="#ip-info-modal" data-ip="${data['ip_address']}">
@@ -306,14 +335,16 @@ DOCUMENTATION :: END
<div class="sub-value time-right"> <div class="sub-value time-right">
% if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown': % if data['media_type'] != 'photo' and data['bandwidth'] != 'Unknown':
<% <%
bw = helpers.cast_to_int(data['bandwidth']) bw = cast_to_int(data['bandwidth'])
if bw > 1000: if bw > 1000000:
bw = str(round(bw / 1000000.0, 1)) + ' Gbps'
elif bw > 1000:
bw = str(round(bw / 1000.0, 1)) + ' Mbps' bw = str(round(bw / 1000.0, 1)) + ' Mbps'
else: else:
bw = str(bw) + ' kbps' bw = str(bw) + ' kbps'
%> %>
<span id="stream-bandwidth-${sk}">${bw}</span> <span id="stream-bandwidth-${sk}">${bw}</span>
<span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Required Bandwidth)"><i class="fa fa-info-circle"></i></span> <span id="streaming-brain-${sk}" data-toggle="tooltip" title="Streaming Brain Estimate (Reserved Bandwidth)"><i class="fa fa-info-circle"></i></span>
% elif data['synced_version'] == 1 or data['channel_stream'] == 1: % elif data['synced_version'] == 1 or data['channel_stream'] == 1:
<span id="stream-bandwidth-${sk}">None</span> <span id="stream-bandwidth-${sk}">None</span>
% else: % else:
@@ -326,8 +357,8 @@ DOCUMENTATION :: END
</div> </div>
% if data['media_type'] != 'photo': % if data['media_type'] != 'photo':
<div class="dashboard-activity-info-time"> <div class="dashboard-activity-info-time">
% if data['live'] == 1: % if data['live']:
<br />Live <br /><span class="thumb-tooltip" data-toggle="popover" data-img="${data['channel_thumb']}" data-height="40" data-width="40">${data['channel_call_sign']} ${data['channel_identifier']}</span>
% elif data['view_offset']: % elif data['view_offset']:
ETA: ETA:
<span id="stream-eta-${sk}"> <span id="stream-eta-${sk}">
@@ -356,8 +387,8 @@ DOCUMENTATION :: END
</div> </div>
<div class="dashboard-activity-progress"> <div class="dashboard-activity-progress">
<div class="dashboard-activity-progress-bar"> <div class="dashboard-activity-progress-bar">
% if data['live'] == 1: % if data['live']:
<div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-toggle="tooltip" title="Stream Progress Live">Live</div> <div id="progress-bar-${sk}" class="progress-bar" style="width: 100%" data-state="live" data-toggle="tooltip" title="Stream Progress Live">Live</div>
% else: % 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="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> <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>
@@ -366,7 +397,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
<div class="dashboard-activity-metadata-wrapper"> <div class="dashboard-activity-metadata-wrapper">
<a href="${user_href}" title="${data['friendly_name']}"> <a href="${user_href}" title="${data['username']}">
<div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div> <div class="dashboard-activity-metadata-user-thumb" style="background-image: url(${data['user_thumb']});"></div>
</a> </a>
<div class="dashboard-activity-metadata-title-container"> <div class="dashboard-activity-metadata-title-container">
@@ -377,10 +408,21 @@ DOCUMENTATION :: END
<i class="fa fa-fw fa-pause"></i>&nbsp; <i class="fa fa-fw fa-pause"></i>&nbsp;
% elif data['state'] == 'buffering': % elif data['state'] == 'buffering':
<i class="fa fa-fw fa-spinner"></i>&nbsp; <i class="fa fa-fw fa-spinner"></i>&nbsp;
% elif data['state'] == 'error':
<i class="fa fa-fw fa-exclamation-triangle"></i>&nbsp;
% endif % endif
</div> </div>
<div class="dashboard-activity-metadata-title"> <div class="dashboard-activity-metadata-title">
% if data['channel_stream'] == 0: % if data['live']:
% if data['media_type'] == 'movie':
<a href="${href}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'episode':
<a href="${href}" title="${data['grandparent_title']}">${data['grandparent_title']}</a>
- <a href="${href}" title="${data['title']}">${data['title']}</a>
% else:
<span title="${data['title']}">${data['title']}</span>
% endif
% elif data['channel_stream'] == 0:
% if data['media_type'] == 'movie': % if data['media_type'] == 'movie':
<a href="${href}" title="${data['title']}">${data['title']}</a> <a href="${href}" title="${data['title']}">${data['title']}</a>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
@@ -405,9 +447,9 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
<div class="dashboard-activity-metadata-subtitle-container"> <div class="dashboard-activity-metadata-subtitle-container">
% if data['live'] == 1: % if data['live']:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Plex Live TV"> <div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="Live TV">
<i class="fa fa-fw fa-television"></i>&nbsp; <i class="fa fa-fw fa-broadcast-tower"></i>&nbsp;
</div> </div>
% elif data['channel_stream'] == 0: % elif data['channel_stream'] == 0:
<div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}"> <div id="media-type-${sk}" class="dashboard-activity-metadata-media_type-icon" title="${data['media_type'].capitalize()}">
@@ -429,8 +471,19 @@ DOCUMENTATION :: END
</div> </div>
% endif % endif
<div class="dashboard-activity-metadata-subtitle"> <div class="dashboard-activity-metadata-subtitle">
% if data['live'] == 1: % if data['live']:
<span title="Plex Live TV" class="sub-heading">Plex Live TV</span> % if data['media_type'] == 'movie':
<span title="${data['year']}" class="sub-heading">${data['year']}</span>
% elif data['media_type'] == 'episode':
% if data['media_index']:
<a href="${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>
% else:
<a href="${href}" title="${data['originally_available_at']}" class="sub-heading">${data['originally_available_at']}</a>
% endif
% else:
<span title="Live TV" class="sub-heading">Live TV</span>
% endif
% elif data['channel_stream'] == 0: % elif data['channel_stream'] == 0:
% if data['media_type'] == 'movie': % if data['media_type'] == 'movie':
<span title="${data['year']}" class="sub-heading">${data['year']}</span> <span title="${data['year']}" class="sub-heading">${data['year']}</span>
@@ -468,7 +521,7 @@ DOCUMENTATION :: END
% endif % endif
</div> </div>
<div class="dashboard-activity-metadata-user"> <div class="dashboard-activity-metadata-user">
<a href="${user_href}" title="${data['friendly_name']}">${data['friendly_name']}</a> <a href="${user_href}" title="${data['username']}">${data['friendly_name']}</a>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -21,6 +21,7 @@ parent_count Returns the parent item count for the library.
child_count Returns the child item count for the library. child_count Returns the child item count for the library.
do_notify Returns bool value for whether to send notifications for the library. do_notify Returns bool value for whether to send notifications for the library.
keep_history Returns bool value for whether to keep history for the library. keep_history Returns bool value for whether to keep history for the library.
deleted_section Returns bool value for whether the library is marked as deleted.
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%doc>
@@ -39,13 +40,22 @@ DOCUMENTATION :: END
<div class="modal-body" id="modal-text"> <div class="modal-body" id="modal-text">
<fieldset> <fieldset>
<div class="form-group"> <div class="form-group">
<label for="profile_url">Library Picture URL</label> <label for="profile_url">Library Thumbnail URL</label>
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<input type="text" class="form-control" id="custom_thumb_url" name="custom_thumb_url" value="${data['library_thumb']}"> <input type="text" class="form-control" id="custom_thumb_url" name="custom_thumb_url" value="${data['library_thumb']}">
</div> </div>
</div> </div>
<p class="help-block">Change the library's picture in Tautulli. To reset to default, leave this field empty and save.</p> <p class="help-block">Change the library's thumbnail in Tautulli. To reset to default, leave this field empty and save.</p>
</div>
<div class="form-group">
<label for="profile_url">Library Background Art URL</label>
<div class="row">
<div class="col-md-8">
<input type="text" class="form-control" id="custom_art_url" name="custom_art_url" value="${data['library_art']}">
</div>
</div>
<p class="help-block">Change the library's background art in Tautulli. To reset to default, leave this field empty and save.</p>
</div> </div>
<div class="checkbox"> <div class="checkbox">
<label> <label>
@@ -59,6 +69,12 @@ DOCUMENTATION :: END
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this library. This is permanent!</p> <p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this library. This is permanent!</p>
</div> </div>
% endif % endif
% if data['deleted_section']:
<div class="form-group">
<button class="btn btn-bright" id="undelete-library">Undelete</button>
<p class="help-block">Click to re-add the library to the Tautulli libraries list.</p>
</div>
% endif
</fieldset> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -73,6 +89,7 @@ DOCUMENTATION :: END
// Save library options // Save library options
$("#save_library").on('click', function () { $("#save_library").on('click', function () {
var custom_thumb = $("#custom_thumb_url").val(); var custom_thumb = $("#custom_thumb_url").val();
var custom_art = $("#custom_art_url").val();
var keep_history = 0; var keep_history = 0;
if ($("#keep_history").is(":checked")) { if ($("#keep_history").is(":checked")) {
keep_history = 1; keep_history = 1;
@@ -83,6 +100,7 @@ DOCUMENTATION :: END
data: { data: {
section_id: '${data["section_id"]}', section_id: '${data["section_id"]}',
custom_thumb: custom_thumb, custom_thumb: custom_thumb,
custom_art: custom_art,
keep_history: keep_history keep_history: keep_history
}, },
cache: false, cache: false,
@@ -97,15 +115,13 @@ DOCUMENTATION :: END
var msg = 'Are you REALLY sure you want to purge all history for the <strong>${data["section_name"]}</strong> library?<br>' + var msg = 'Are you REALLY sure you want to purge all history for the <strong>${data["section_name"]}</strong> library?<br>' +
'This is permanent and cannot be undone!'; 'This is permanent and cannot be undone!';
var url = 'delete_all_library_history'; var url = 'delete_all_library_history';
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); }); confirmAjaxCall(url, msg, { server_id: '${server_id}', section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
}); });
$(document).ready(function() { $('#undelete-library').click(function () {
// Move #confirm-modal to parent container var msg = 'Are you sure you want to undelete this library?';
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) { var url = 'undelete_library';
$('#confirm-modal-purge').appendTo($('#edit-library-modal').parent()); confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
}
$('#edit-library-modal > #confirm-modal-purge').remove();
}); });
</script> </script>
% endif % endif

View File

@@ -21,6 +21,7 @@ is_restricted Returns bool value for whether the user account is restricte
do_notify Returns bool value for whether to send notifications for the user. do_notify Returns bool value for whether to send notifications for the user.
keep_history Returns bool value for whether to keep history for the user. keep_history Returns bool value for whether to keep history for the user.
allow_guest Returns bool value for whether to allow guest access for the user. allow_guest Returns bool value for whether to allow guest access for the user.
deleted_user Returns bool value for whether the user is marked as deleted.
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%doc>
@@ -74,6 +75,12 @@ DOCUMENTATION :: END
<p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p> <p class="help-block">DANGER ZONE! Click the purge button to remove all history logged for this user. This is permanent!</p>
</div> </div>
% endif % endif
% if data['deleted_user']:
<div class="form-group">
<button class="btn btn-bright" id="undelete-user">Undelete</button>
<p class="help-block">Click to re-add the user to the Tautulli users list.</p>
</div>
% endif
</fieldset> </fieldset>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -122,12 +129,10 @@ DOCUMENTATION :: END
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); }); confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
}); });
$(document).ready(function() { $('#undelete-user').click(function () {
// Move #confirm-modal-purge to parent container var msg = 'Are you sure you want to undelete this user?';
if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) { var url = 'undelete_user';
$('#confirm-modal-purge').appendTo($('#edit-user-modal').parent()); confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
}
$('#edit-user-modal > #confirm-modal-purge').remove();
}); });
</script> </script>
% endif % endif

View File

@@ -252,6 +252,7 @@
case "TV": media_type = 'episode'; break; case "TV": media_type = 'episode'; break;
case "Movies": media_type = 'movie'; break; case "Movies": media_type = 'movie'; break;
case "Music": media_type = 'track'; break; case "Music": media_type = 'track'; break;
case "Live TV": media_type = 'live'; break;
case "Direct Play": transcode_decision = 'direct play'; break; case "Direct Play": transcode_decision = 'direct play'; break;
case "Direct Stream": transcode_decision = 'copy'; break; case "Direct Stream": transcode_decision = 'copy'; break;
case "Transcode": transcode_decision = 'transcode'; break; case "Transcode": transcode_decision = 'transcode'; break;
@@ -304,18 +305,35 @@
setLocalStorage(chart_key, JSON.stringify(chart_visibility)); setLocalStorage(chart_key, JSON.stringify(chart_visibility));
} }
function getGraphColors(data_series) {
var colors = {
'TV': '#E5A00D',
'Movies': '#FFFFFF',
'Music': '#F06464',
'Live TV': '#19A0D7',
'Direct Play': '#E5A00D',
'Direct Stream': '#FFFFFF',
'Transcode': '#F06464'
};
var series_colors = [];
$.each(data_series, function(index, series) {
series_colors.push(colors[series.name]);
});
return series_colors;
}
</script> </script>
<script src="${http_root}js/graphs/plays_by_day.js"></script> <script src="${http_root}js/graphs/plays_by_day.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_dayofweek.js"></script> <script src="${http_root}js/graphs/plays_by_dayofweek.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_hourofday.js"></script> <script src="${http_root}js/graphs/plays_by_hourofday.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_platform.js"></script> <script src="${http_root}js/graphs/plays_by_platform.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_user.js"></script> <script src="${http_root}js/graphs/plays_by_user.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_stream_type.js"></script> <script src="${http_root}js/graphs/plays_by_stream_type.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_source_resolution.js"></script> <script src="${http_root}js/graphs/plays_by_source_resolution.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_stream_resolution.js"></script> <script src="${http_root}js/graphs/plays_by_stream_resolution.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_platform_by_stream_type.js"></script> <script src="${http_root}js/graphs/plays_by_platform_by_stream_type.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_user_by_stream_type.js"></script> <script src="${http_root}js/graphs/plays_by_user_by_stream_type.js${cache_param}"></script>
<script src="${http_root}js/graphs/plays_by_month.js"></script> <script src="${http_root}js/graphs/plays_by_month.js${cache_param}"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
@@ -390,6 +408,7 @@
hc_plays_by_day_options.yAxis.min = 0; hc_plays_by_day_options.yAxis.min = 0;
hc_plays_by_day_options.xAxis.categories = dateArray; hc_plays_by_day_options.xAxis.categories = dateArray;
hc_plays_by_day_options.series = getGraphVisibility(hc_plays_by_day_options.chart.renderTo, data.series); hc_plays_by_day_options.series = getGraphVisibility(hc_plays_by_day_options.chart.renderTo, data.series);
hc_plays_by_day_options.colors = getGraphColors(data.series);
var hc_plays_by_day = new Highcharts.Chart(hc_plays_by_day_options); var hc_plays_by_day = new Highcharts.Chart(hc_plays_by_day_options);
} }
}); });
@@ -403,6 +422,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_dayofweek_options.xAxis.categories = data.categories; hc_plays_by_dayofweek_options.xAxis.categories = data.categories;
hc_plays_by_dayofweek_options.series = getGraphVisibility(hc_plays_by_dayofweek_options.chart.renderTo, data.series); hc_plays_by_dayofweek_options.series = getGraphVisibility(hc_plays_by_dayofweek_options.chart.renderTo, data.series);
hc_plays_by_dayofweek_options.colors = getGraphColors(data.series);
var hc_plays_by_dayofweek = new Highcharts.Chart(hc_plays_by_dayofweek_options); var hc_plays_by_dayofweek = new Highcharts.Chart(hc_plays_by_dayofweek_options);
} }
}); });
@@ -416,6 +436,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_hourofday_options.xAxis.categories = data.categories; hc_plays_by_hourofday_options.xAxis.categories = data.categories;
hc_plays_by_hourofday_options.series = getGraphVisibility(hc_plays_by_hourofday_options.chart.renderTo, data.series); hc_plays_by_hourofday_options.series = getGraphVisibility(hc_plays_by_hourofday_options.chart.renderTo, data.series);
hc_plays_by_hourofday_options.colors = getGraphColors(data.series);
var hc_plays_by_hourofday = new Highcharts.Chart(hc_plays_by_hourofday_options); var hc_plays_by_hourofday = new Highcharts.Chart(hc_plays_by_hourofday_options);
} }
}); });
@@ -429,6 +450,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_platform_options.xAxis.categories = data.categories; hc_plays_by_platform_options.xAxis.categories = data.categories;
hc_plays_by_platform_options.series = getGraphVisibility(hc_plays_by_platform_options.chart.renderTo, data.series); hc_plays_by_platform_options.series = getGraphVisibility(hc_plays_by_platform_options.chart.renderTo, data.series);
hc_plays_by_platform_options.colors = getGraphColors(data.series);
var hc_plays_by_platform = new Highcharts.Chart(hc_plays_by_platform_options); var hc_plays_by_platform = new Highcharts.Chart(hc_plays_by_platform_options);
} }
}); });
@@ -442,6 +464,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_user_options.xAxis.categories = data.categories; hc_plays_by_user_options.xAxis.categories = data.categories;
hc_plays_by_user_options.series = getGraphVisibility(hc_plays_by_user_options.chart.renderTo, data.series); hc_plays_by_user_options.series = getGraphVisibility(hc_plays_by_user_options.chart.renderTo, data.series);
hc_plays_by_user_options.colors = getGraphColors(data.series);
var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options); var hc_plays_by_user = new Highcharts.Chart(hc_plays_by_user_options);
} }
}); });
@@ -478,6 +501,7 @@
hc_plays_by_stream_type_options.yAxis.min = 0; hc_plays_by_stream_type_options.yAxis.min = 0;
hc_plays_by_stream_type_options.xAxis.categories = dateArray; hc_plays_by_stream_type_options.xAxis.categories = dateArray;
hc_plays_by_stream_type_options.series = getGraphVisibility(hc_plays_by_stream_type_options.chart.renderTo, data.series); hc_plays_by_stream_type_options.series = getGraphVisibility(hc_plays_by_stream_type_options.chart.renderTo, data.series);
hc_plays_by_stream_type_options.colors = getGraphColors(data.series);
var hc_plays_by_stream_type = new Highcharts.Chart(hc_plays_by_stream_type_options); var hc_plays_by_stream_type = new Highcharts.Chart(hc_plays_by_stream_type_options);
} }
}); });
@@ -491,6 +515,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_source_resolution_options.xAxis.categories = data.categories; hc_plays_by_source_resolution_options.xAxis.categories = data.categories;
hc_plays_by_source_resolution_options.series = getGraphVisibility(hc_plays_by_source_resolution_options.chart.renderTo, data.series); hc_plays_by_source_resolution_options.series = getGraphVisibility(hc_plays_by_source_resolution_options.chart.renderTo, data.series);
hc_plays_by_source_resolution_options.colors = getGraphColors(data.series);
var hc_plays_by_source_resolution = new Highcharts.Chart(hc_plays_by_source_resolution_options); var hc_plays_by_source_resolution = new Highcharts.Chart(hc_plays_by_source_resolution_options);
} }
}); });
@@ -504,6 +529,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_stream_resolution_options.xAxis.categories = data.categories; hc_plays_by_stream_resolution_options.xAxis.categories = data.categories;
hc_plays_by_stream_resolution_options.series = getGraphVisibility(hc_plays_by_stream_resolution_options.chart.renderTo, data.series); hc_plays_by_stream_resolution_options.series = getGraphVisibility(hc_plays_by_stream_resolution_options.chart.renderTo, data.series);
hc_plays_by_stream_resolution_options.colors = getGraphColors(data.series);
var hc_plays_by_stream_resolution = new Highcharts.Chart(hc_plays_by_stream_resolution_options); var hc_plays_by_stream_resolution = new Highcharts.Chart(hc_plays_by_stream_resolution_options);
} }
}); });
@@ -517,6 +543,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_platform_by_stream_type_options.xAxis.categories = data.categories; hc_plays_by_platform_by_stream_type_options.xAxis.categories = data.categories;
hc_plays_by_platform_by_stream_type_options.series = getGraphVisibility(hc_plays_by_platform_by_stream_type_options.chart.renderTo, data.series); hc_plays_by_platform_by_stream_type_options.series = getGraphVisibility(hc_plays_by_platform_by_stream_type_options.chart.renderTo, data.series);
hc_plays_by_platform_by_stream_type_options.colors = getGraphColors(data.series);
var hc_plays_by_platform_by_stream_type = new Highcharts.Chart(hc_plays_by_platform_by_stream_type_options); var hc_plays_by_platform_by_stream_type = new Highcharts.Chart(hc_plays_by_platform_by_stream_type_options);
} }
}); });
@@ -530,6 +557,7 @@
if (yaxis === 'duration') { dataSecondsToHours(data); } if (yaxis === 'duration') { dataSecondsToHours(data); }
hc_plays_by_user_by_stream_type_options.xAxis.categories = data.categories; hc_plays_by_user_by_stream_type_options.xAxis.categories = data.categories;
hc_plays_by_user_by_stream_type_options.series = getGraphVisibility(hc_plays_by_user_by_stream_type_options.chart.renderTo, data.series); hc_plays_by_user_by_stream_type_options.series = getGraphVisibility(hc_plays_by_user_by_stream_type_options.chart.renderTo, data.series);
hc_plays_by_user_by_stream_type_options.colors = getGraphColors(data.series);
var hc_plays_by_user_by_stream_type = new Highcharts.Chart(hc_plays_by_user_by_stream_type_options); var hc_plays_by_user_by_stream_type = new Highcharts.Chart(hc_plays_by_user_by_stream_type_options);
} }
}); });
@@ -553,6 +581,7 @@
hc_plays_by_month_options.yAxis.min = 0; hc_plays_by_month_options.yAxis.min = 0;
hc_plays_by_month_options.xAxis.categories = data.categories; hc_plays_by_month_options.xAxis.categories = data.categories;
hc_plays_by_month_options.series = getGraphVisibility(hc_plays_by_month_options.chart.renderTo, data.series); hc_plays_by_month_options.series = getGraphVisibility(hc_plays_by_month_options.chart.renderTo, data.series);
hc_plays_by_month_options.colors = getGraphColors(data.series);
var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options); var hc_plays_by_month = new Highcharts.Chart(hc_plays_by_month_options);
} }
}); });

View File

@@ -8,6 +8,13 @@
<%def name="body()"> <%def name="body()">
<div class='container-fluid'> <div class='container-fluid'>
% if config['database_is_importing']:
<div style="text-align: center; margin-top: 20px;">
<i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is importing history from another database. This could take a few minutes depending on the size of your database.
<br />
You may leave this page and check back later.
</div>
% endif
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
<span><i class="fa fa-history"></i> History</span> <span><i class="fa fa-history"></i> History</span>
@@ -44,6 +51,9 @@
<label class="btn btn-dark"> <label class="btn btn-dark">
<input type="radio" name="media_type-filter" id="history-track" value="track" autocomplete="off"> Music <input type="radio" name="media_type-filter" id="history-track" value="track" autocomplete="off"> Music
</label> </label>
<label class="btn btn-dark">
<input type="radio" name="media_type-filter" id="history-live" value="live" autocomplete="off"> Live TV
</label>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button> <button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
@@ -60,7 +70,8 @@
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="device">Player</th> <th align="left" id="product">Product</th>
<th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
<th align="left" id="paused_counter">Paused</th> <th align="left" id="paused_counter">Paused</th>
@@ -143,7 +154,7 @@
var colvis = new $.fn.dataTable.ColVis(history_table, { var colvis = new $.fn.dataTable.ColVis(history_table, {
buttonText: '<i class="fa fa-columns"></i> Select columns', buttonText: '<i class="fa fa-columns"></i> Select columns',
buttonClass: 'btn btn-dark', buttonClass: 'btn btn-dark',
exclude: [0, 11] exclude: [0, 12]
}); });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
@@ -181,19 +192,17 @@
$('#deleteCount').text(history_to_delete.length); $('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_history_rows',
url: 'delete_history_rows', type: 'POST',
type: 'POST', data: { row_ids: history_to_delete.join(',') },
data: { row_id: row }, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "History deleted";
var msg = "History deleted"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); history_table.draw();
} }
});
}); });
history_table.draw();
}); });
} }

View File

@@ -6,7 +6,7 @@
<h4 class="modal-title" id="myModalLabel"> <h4 class="modal-title" id="myModalLabel">
<strong><span id="modal_header_ip_address"> <strong><span id="modal_header_ip_address">
% if data.get('media_type'): % if data.get('media_type'):
<% h = {'episode': 'TV Show', 'track': 'Music'} %> <% h = {'episode': 'TV Show', 'track': 'Music', 'live': 'Live TV'} %>
<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> <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'): % elif data.get('transcode_decision'):
<% h = {'copy': 'Direct Stream'} %> <% h = {'copy': 'Direct Stream'} %>
@@ -26,6 +26,7 @@
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="device">Player</th> <th align="left" id="device">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -54,14 +55,14 @@
json_data: JSON.stringify(d), json_data: JSON.stringify(d),
user_id: "${data['user_id']}", user_id: "${data['user_id']}",
start_date: "${data['start_date']}", start_date: "${data['start_date']}",
media_type: "${data.get('media_type')}", media_type: "${data.get('media_type') or 'all'}",
transcode_decision: "${data.get('transcode_decision')}" transcode_decision: "${data.get('transcode_decision')}"
}; };
} }
}; };
history_table = $('#history_table_modal').DataTable(history_table_options); history_table = $('#history_table_modal').DataTable(history_table_options);
history_table.columns([0, 3, 4, 8, 10, 11]).visible(false); history_table.columns([0, 3, 4, 5, 9, 11, 12]).visible(false);
clearSearchButton('history_table_modal', history_table); clearSearchButton('history_table_modal', history_table);

View File

@@ -53,11 +53,11 @@ DOCUMENTATION :: END
</%doc> </%doc>
<%! <%!
from plexpy import helpers from plexpy.helpers import cast_to_int, page
# Human readable duration # Human readable duration
def hd(seconds): def hd(seconds):
m, s = divmod(helpers.cast_to_int(seconds), 60) m, s = divmod(cast_to_int(seconds), 60)
h, m = divmod(m, 60) h, m = divmod(m, 60)
return str(h).zfill(1) + ':' + str(m).zfill(2) return str(h).zfill(1) + ':' + str(m).zfill(2)
%> %>
@@ -72,11 +72,8 @@ DOCUMENTATION :: END
<div class="dashboard-stats-instance" id="stats-instance-${stat_id}" data-stat_id="${stat_id}"> <div class="dashboard-stats-instance" id="stats-instance-${stat_id}" data-stat_id="${stat_id}">
<div class="dashboard-stats-container"> <div class="dashboard-stats-container">
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'): % if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
% if row0['art']: <% fallback = 'art-live' if row0['live'] else '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);"> <div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(${page('pms_image_proxy', row0['art'], row0['rating_key'], 500, 280, 40, '282828', 3, fallback=fallback)});">
% else:
<div id="stats-background-${stat_id}" class="dashboard-stats-background" style="background-image: url(images/art.png);">
% endif
% elif stat_id == 'top_platforms': % elif stat_id == 'top_platforms':
<div id="stats-background-${stat_id}" class="dashboard-stats-background platform-${row0['platform_name']}-rgba no-image"> <div id="stats-background-${stat_id}" class="dashboard-stats-background platform-${row0['platform_name']}-rgba no-image">
% else: % else:
@@ -85,21 +82,29 @@ DOCUMENTATION :: END
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'): % 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"> <div class="dashboard-stats-poster-container hidden-xs">
% if stat_id in ('top_music', 'popular_music'): % if stat_id in ('top_music', 'popular_music'):
<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> <div id="stats-thumb-${stat_id}-bg" class="dashboard-stats-poster" style="background-image: url(${page('pms_image_proxy', row0['thumb'], row0['rating_key'], 300, 300, 60, '282828', 3, fallback='cover')});"></div>
% endif % endif
<% 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 '#' %> height, fallback = ('450', 'poster')
if stat_id in ('top_music', 'popular_music'):
height, fallback = ('300', 'cover')
elif row0['live']:
height, fallback = ('450', 'poster-live')
href = '#'
if row0['rating_key']:
if row0['live']:
href = page('info', row0['rating_key'], row0['guid'], history=True, live=row0['live'])
else:
href = page('info', row0['rating_key'])
%>
<a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}"> <a id="stats-thumb-url-${stat_id}" href="${href}" title="${row0['title']}">
% if row0['thumb']: <div id="stats-thumb-${stat_id}" class="dashboard-stats-${fallback.split('-')[0]}" style="background-image: url(${page('pms_image_proxy', row0['thumb'], row0['rating_key'], 300, height, fallback=fallback)});"></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> </a>
</div> </div>
% elif stat_id == 'top_users': % elif stat_id == 'top_users':
<% user_href = 'user?user_id={}'.format(row0['user_id']) if row0['user_id'] else '#' %> <% user_href = page('user', 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"> <a id="stats-thumb-url-${stat_id}" href="${user_href}" title="${row0['user']}" 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> <div id="stats-thumb-${stat_id}" class="dashboard-stats-circle" style="background-image: url(${row0['user_thumb'] or 'images/gravatar-default.png'})"></div>
</a> </a>
% elif stat_id == 'top_platforms': % elif stat_id == 'top_platforms':
@@ -117,7 +122,7 @@ DOCUMENTATION :: END
% elif stat_id.startswith('popular'): % elif stat_id.startswith('popular'):
<span class="dashboard-stats-stats-units">users</span> <span class="dashboard-stats-stats-units">users</span>
% elif stat_id == 'last_watched': % elif stat_id == 'last_watched':
<span class="dashboard-stats-stats-units" id="last-watched-header-info">${row0['friendly_name']}</span> <span class="dashboard-stats-stats-units" id="last-watched-header-info" title="${row0['user']}">${row0['friendly_name']}</span>
% elif stat_id == 'most_concurrent': % elif stat_id == 'most_concurrent':
<span class="dashboard-stats-stats-units" id="most-concurrent-header-info">streams</span> <span class="dashboard-stats-stats-units" id="most-concurrent-header-info">streams</span>
% endif % endif
@@ -126,20 +131,28 @@ DOCUMENTATION :: END
<div class="dashboard-stats-info scoller-content"> <div class="dashboard-stats-info scoller-content">
<ul class="list-unstyled dashboard-stats-info-list"> <ul class="list-unstyled dashboard-stats-info-list">
% for row in top_stat['rows']: % 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')}" <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-guid="${row.get('guid')}" data-title="${row.get('title')}"
data-art="${row.get('art')}" data-thumb="${row.get('thumb')}" data-platform="${row.get('platform_name')}" 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_thumb="${row.get('user_thumb')}" data-user_id="${row.get('user_id')}" data-user="${row.get('user')}" 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')}"> data-last_watch="${row.get('last_watch')}" data-started="${row.get('started')}" data-live="${row.get('live')}">
<div class="sub-list">${loop.index + 1}</div> <div class="sub-list">${loop.index + 1}</div>
<div class="sub-value"> <div class="sub-value">
% if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'): % if stat_id in ('top_movies', 'popular_movies', 'top_tv', 'popular_tv', 'top_music', 'popular_music', 'last_watched'):
<% href = 'info?rating_key={}'.format(row['rating_key']) if row['rating_key'] else '#' %> <%
href = '#'
if row['rating_key']:
if row['live']:
href = page('info', row['rating_key'], row['guid'], history=True, live=row['live'])
else:
href = page('info', row['rating_key'])
%>
<a href="${href}" title="${row['title']}"> <a href="${href}" title="${row['title']}">
${row['title']} ${row['title']}
</a> </a>
% elif stat_id == 'top_users': % elif stat_id == 'top_users':
<% user_href = 'user?user_id={}'.format(row['user_id']) if row['user_id'] else '#' %> <% user_href = page('user', row['user_id']) if row['user_id'] else '#' %>
<a href="${user_href}" title="${row['friendly_name']}"> <a href="${user_href}" title="${row['user']}">
${row['friendly_name']} ${row['friendly_name']}
</a> </a>
% elif stat_id == 'top_platforms': % elif stat_id == 'top_platforms':
@@ -170,78 +183,6 @@ DOCUMENTATION :: END
</div> </div>
% endif % endif
% endfor % endfor
<script>
$('.dashboard-stats-instance .dashboard-stats-info-scroller').scrollbar()
function changeImages(elem) {
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(' + (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') + '-rgba');
} else {
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&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 + '&opacity=60&background=282828&blur=3&fallback=' + fallback + ')');
} else {
$('#stats-thumb-' + stat_id).css('background-image', 'url(images/' + fallback + '.png)');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(images/' + fallback + '.png)');
}
}
}
$('.dashboard-stats-info-item').mouseenter(function () {
changeImages(this)
if ($(this).data('stat_id') == 'last_watched') {
var friendly_name = $(this).data('friendly_name');
var last_watch = moment($(this).data('last_watch'), 'X').format(date_format);
$('#last-watched-header-info').html(friendly_name);
} else if ($(this).data('stat_id') == 'most_concurrent') {
var started = moment($(this).data('started'), 'X').format(date_format + ' ' + time_format);
$('#most-concurrent-header-info').html(started);
}
});
$('.dashboard-stats-instance').mouseleave(function () {
changeImages($(this).find('.dashboard-stats-info-item').first())
if ($(this).data('stat_id') == 'last_watched') {
$('#last-watched-header-info').text($(this).find('.dashboard-stats-info-item').first().data('friendly_name'));
} else if ($(this).data('stat_id') == 'most_concurrent') {
$('#most-concurrent-header-info').text('streams');
}
});
</script>
% else: % else:
<div class="text-muted">No stats to show for the selected period.</div><br> <div class="text-muted">No stats to show for the selected period.</div><br>
% endif % endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -2,7 +2,7 @@
<browserconfig> <browserconfig>
<msapplication> <msapplication>
<tile> <tile>
<square150x150logo src="${http_root}images/favicon/mstile-150x150.png?v=2.0.5"/> <square150x150logo src="mstile-150x150.png?v=2.0.5"/>
<TileColor>#282a2d</TileColor> <TileColor>#282a2d</TileColor>
</tile> </tile>
</msapplication> </msapplication>

View File

@@ -1,18 +1,23 @@
{ {
"name": "Tautulli", "name": "Tautulli: Monitor your Plex Media Server",
"short_name": "Tautulli",
"Description": "A Python based monitoring and tracking tool for Plex Media Server.",
"start_url": "../../",
"scope": "../../",
"icons": [ "icons": [
{ {
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.5", "src": "android-chrome-192x192.png?v=2.0.5",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.5", "src": "android-chrome-256x256.png?v=2.0.5",
"sizes": "256x256", "sizes": "256x256",
"type": "image/png" "type": "image/png"
} }
], ],
"theme_color": "#282a2d", "theme_color": "#282a2d",
"background_color": "#282a2d", "background_color": "#282a2d",
"display": "standalone" "display": "standalone",
"orientation": "any"
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -0,0 +1,9 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<title>live</title>
<path fill="#fff" d="M9.636 10.115c-0.829 0.544-1.243 0.816-2.072 1.361-2.331-3.547-2.331-6.195 0-9.749 0.829 0.546 1.244 0.819 2.072 1.361-1.68 2.557-1.68 4.464 0 7.027z"></path>
<path fill="#fff" d="M4.374 11.662c-0.828 0.542-1.243 0.815-2.072 1.359-3.069-4.676-3.069-8.159 0-12.838 0.829 0.546 1.244 0.817 2.072 1.362-2.418 3.684-2.418 6.426 0 10.117z"></path>
<path fill="#fff" d="M22.365 10.115c0.826 0.544 1.242 0.816 2.070 1.361 2.334-3.547 2.334-6.195 0-9.749-0.828 0.546-1.244 0.819-2.070 1.361 1.677 2.557 1.677 4.464 0 7.027z"></path>
<path fill="#fff" d="M27.627 11.662c0.827 0.542 1.243 0.815 2.070 1.359 3.070-4.676 3.070-8.159 0-12.838-0.827 0.546-1.243 0.817-2.070 1.362 2.419 3.684 2.419 6.426 0 10.117z"></path>
<path fill="#fff" d="M25.211 31.982l2.611-0.95-8.172-22.45c0.32-0.589 0.502-1.263 0.502-1.979 0-2.293-1.859-4.152-4.152-4.152s-4.151 1.858-4.151 4.152c0 0.672 0.16 1.305 0.443 1.868l-8.212 22.561 2.612 0.95 1.952-5.362h14.616l1.951 5.362zM17.396 10.513l3.945 10.834-7.903-7.9 1.080-2.966c0.46 0.176 0.96 0.272 1.481 0.272 0.49 0.001 0.961-0.084 1.397-0.24zM12.39 16.329l7.51 7.512h-10.245l2.735-7.512z"></path>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

View File

@@ -0,0 +1,7 @@
<!-- Generated by IcoMoon.io -->
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<title>lg</title>
<path fill="#fff" d="M30.203 31.797c0 8.176-6.654 14.832-14.835 14.82-7.927-0.011-14.818-6.28-14.812-14.838 0.005-8.282 6.541-14.82 14.841-14.803 8.618 0.017 14.807 6.969 14.806 14.822zM26.577 32.388c-0.087 4.433-3.485 9.518-9.37 10.487-6.122 1.008-11.584-2.989-12.814-8.656-0.632-2.912-0.221-5.696 1.362-8.228 2.347-3.754 5.815-5.502 10.222-5.453 0-0.387 0-0.761 0-1.134-4.114-0.281-9.226 1.824-11.763 6.923-2.454 4.932-1.296 10.953 2.811 14.672 4.153 3.762 10.224 4.309 14.953 1.326 2.328-1.468 3.999-3.496 4.997-6.067 0.628-1.617 0.882-3.296 0.813-5.032-2.967 0-5.909 0-8.864 0 0 0.39 0 0.768 0 1.162 2.558-0 5.097-0 7.652-0zM15.991 37.112c0-0.129 0-0.221 0-0.313 0-3.731 0-7.463 0-11.194 0-0.060-0.004-0.119 0-0.179 0.009-0.118-0.038-0.166-0.16-0.163-0.278 0.006-0.556 0.012-0.833-0.002-0.178-0.008-0.237 0.042-0.237 0.23 0.005 4.194 0.005 8.389-0 12.583-0 0.198 0.065 0.239 0.249 0.237 1.224-0.007 2.448-0.004 3.672-0.004 0.072 0 0.143 0 0.244 0 0-0.343-0.008-0.665 0.003-0.987 0.006-0.166-0.050-0.214-0.214-0.212-0.82 0.007-1.641 0.003-2.461 0.003-0.078 0-0.155 0-0.263 0zM12.434 27.068c0.003-0.987-0.799-1.798-1.785-1.805s-1.799 0.796-1.805 1.782c-0.006 0.985 0.799 1.8 1.783 1.805 0.985 0.004 1.804-0.803 1.807-1.783z"></path>
<path fill="#fff" d="M63.467 30.606c0 2.864 0 5.707 0 8.571-1.242 0-2.479 0-3.742 0 0-0.468 0-0.933 0-1.433-0.203 0.226-0.366 0.432-0.553 0.612-0.683 0.656-1.518 1-2.441 1.136-1.187 0.174-2.348 0.075-3.462-0.4-1.234-0.526-2.145-1.407-2.8-2.565-0.599-1.058-0.906-2.207-1.035-3.409-0.148-1.367-0.103-2.723 0.28-4.051 0.797-2.764 2.635-4.391 5.453-4.899 1.534-0.277 3.058-0.208 4.54 0.311 1.243 0.436 2.298 1.139 3.011 2.276 0.431 0.688 0.584 1.467 0.687 2.258 0.013 0.097 0.028 0.195 0.046 0.318-0.064 0.003-0.126 0.010-0.188 0.010-1.389 0.001-2.779-0.002-4.169 0.003-0.151 0.001-0.215-0.034-0.245-0.197-0.229-1.234-1.281-1.773-2.308-1.679-1.182 0.108-1.823 0.859-2.22 1.888-0.211 0.547-0.315 1.12-0.352 1.703-0.066 1.061-0.039 2.117 0.31 3.138 0.211 0.618 0.523 1.173 1.050 1.579 1.371 1.055 3.326 0.436 3.877-1.228 0.090-0.274 0.157-0.557 0.246-0.875-0.112 0-0.182 0-0.251 0-0.794 0-1.588-0.005-2.382 0.004-0.168 0.002-0.213-0.053-0.212-0.214 0.006-0.887 0.005-1.774 0.001-2.66-0.001-0.139 0.019-0.215 0.192-0.215 2.17 0.006 4.341 0.004 6.511 0.004 0.045 0 0.090 0.008 0.157 0.013z"></path>
<path fill="#fff" d="M48.501 35.522c0 1.233 0 2.44 0 3.661-3.613 0-7.216 0-10.834 0 0-4.923 0-9.841 0-14.77 1.44 0 2.872 0 4.331 0 0 0.092 0 0.175 0 0.259 0 3.526 0 7.053 0 10.579 0 0.271 0 0.271 0.267 0.271 1.985 0 3.97 0 5.954 0 0.086 0 0.171 0 0.281 0z"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@@ -0,0 +1 @@
<svg viewBox="0 0 1000 560" xmlns="http://www.w3.org/2000/svg" stroke-miterlimit="1.414" clip-rule="evenodd" fill-rule="evenodd" stroke-linejoin="round"><path d="M0 89.996C0 62.384 22.378 40 49.997 40h900.006C977.616 40 1000 62.388 1000 89.996v380.008c0 27.612-22.378 49.996-49.997 49.996H49.997C22.384 520 0 497.612 0 470.004V89.996z" fill="#e1be00"/><path d="M769.68 134.76v94.64c6.03-6.976 12.753-12.181 20.17-15.61 7.419-3.428 18.552-5.157 27.24-5.157 10.01 0 18.685 1.552 26.04 4.667 7.362 3.109 12.967 7.471 16.829 13.08 3.857 5.614 6.172 11.11 6.962 16.485.781 5.377 1.176 16.843 1.176 34.41v81.63c0 17.448-1.176 30.434-3.528 38.981-2.357 8.543-7.881 15.958-16.567 22.23-8.691 6.267-19 9.405-30.952 9.405-8.567 0-19.648-1.857-27.07-5.581-7.424-3.724-14.21-9.314-20.362-16.767l-4.709 18.538h-68.04v-290.95h72.809m-631.58 290.95h75.58v-290.95h-75.58v290.95m199.38-290.95c2.881 17.615 5.9 38.29 9.06 62.01l10.829 73.915 17.505-135.92h98.73v290.95h-65.99l-.239-196.38-26.433 196.38h-47.15l-27.862-192.11-.238 192.11h-66.2v-290.95h97.99m218.36 0c36.581 0 57.629 1.681 70.52 5.03 12.895 3.347 22.705 8.847 29.419 16.504 6.719 7.657 10.915 16.181 12.595 25.567 1.677 9.39 2.752 27.843 2.752 55.36v102.18c0 26.08-1.461 43.519-3.918 52.31-2.462 8.8-6.748 15.676-12.862 20.638-6.124 4.962-13.676 8.433-22.672 10.404-9 1.977-22.551 2.962-40.657 2.962h-91.57v-290.95h56.39m239.33 220.35c0 14.08-.7 22.977-2.096 26.677-1.4 3.704-7.485 5.566-12.1 5.566-4.5 0-7.5-1.786-9.02-5.371-1.519-3.581-2.272-11.757-2.272-24.538v-76.891c0-13.257.667-21.519 2-24.809 1.333-3.277 4.248-4.924 8.743-4.924 4.609 0 10.796 1.871 12.376 5.633 1.576 3.762 2.367 11.795 2.367 24.09v74.57m-203.37-167.99c2.986 1.728 4.901 4.457 5.734 8.157.833 3.709 1.257 12.138 1.257 25.29v112.8c0 19.371-1.257 31.23-3.767 35.595-2.509 4.371-9.2 6.548-20.06 6.548v-190.99c8.234 0 13.852.866 16.838 2.6"/></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560" viewBox="0 0 560 560">
<g fill="none" transform="translate(33 140)">
<path fill="#FFF" d="M43.8020066,267.3152 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.3152 Z M266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 C26.3585902,221.21181 24.03,207.991848 22.1723803,189.066133 L266.684852,192.017143 Z M275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.325333 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 L275.03101,89.3083429 Z M302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 Z"/>
<path fill="#00641E" d="M303.565869,264.667352 C306.720846,256.846476 317.903331,252.081752 325.93259,252.63901 C334.515108,253.234819 343.631626,262.264838 345.224872,271.145905 C345.520761,270.823467 345.830656,270.518552 346.145803,270.218895 C348.901593,267.583314 352.35421,265.834438 356.132479,265.352533 C355.554708,262.790552 355.416393,260.048076 355.812079,257.233752 C357.207482,247.3328 365.145698,239.907962 374.234203,239.981562 C380.099449,240.028876 385.245108,243.032457 388.597928,247.646476 C388.897318,247.271467 389.228223,246.928 389.550374,246.577524 C393.384669,226.586362 395.814807,203.999924 396.368066,180.030857 C398.267705,97.6111238 377.375174,30.2776381 349.70522,29.6397714 C322.033515,29.0001524 298.061292,95.2962286 296.161652,177.715962 C296.161652,177.715962 294.696216,207.778057 303.565869,264.667352"/>
<path fill="#FFD700" d="M490.910577,354.797562 C492.545843,352.0656 493.45977,348.7904 493.396741,345.310171 C493.927239,334.065143 486.214879,323.871543 475.484105,325.000076 C475.794,323.713829 475.997095,322.376762 476.075882,320.997638 C476.718433,309.733333 468.957049,300.025143 458.739266,299.315429 C458.515161,299.30141 458.294557,299.2944 458.072203,299.28739 C459.134951,296.492343 459.661948,293.371352 459.47461,290.06461 C458.945862,280.692876 452.488839,272.796648 444.088407,271.242286 C441.054236,270.681524 438.111108,270.960152 435.416597,271.894171 C432.870905,265.617143 427.525652,260.961067 421.023108,259.977981 C420.508367,249.786133 413.153174,241.397486 403.68299,240.740343 C397.728452,240.326781 392.257141,243.064 388.597928,247.646476 C385.245108,243.032457 380.099449,240.030629 374.234203,239.983314 C365.145698,239.907962 357.207482,247.3328 355.812079,257.235505 C355.416393,260.048076 355.554708,262.790552 356.132479,265.354286 C352.35421,265.834438 348.901593,267.585067 346.145803,270.218895 C345.830656,270.518552 345.520761,270.823467 345.224872,271.145905 C343.631626,262.264838 334.515108,253.236571 325.93259,252.63901 C317.903331,252.081752 306.575528,256.963886 303.565869,264.667352 C304.885987,278.101105 313.275915,314.72061 343.621121,347.570743 L343.890748,347.590019 C346.816367,350.239619 350.636656,351.699352 354.709062,351.33661 C357.233744,351.110552 359.558833,350.206324 361.56177,348.811429 L362.050249,348.844724 C364.720249,350.700495 367.929502,351.671314 371.320839,351.369905 C372.616446,351.254248 373.852525,350.942324 375.027325,350.497219 C377.933685,356.51139 384.41522,360.398171 391.649607,359.760305 C397.25223,359.266133 402.014459,356.164419 404.81402,351.799238 L405.720944,351.862324 C408.517003,354.636343 412.233993,356.252038 416.24337,356.131124 C419.557672,361.149943 425.645272,364.237638 432.363167,363.647086 C434.893102,363.424533 437.254957,362.692038 439.361193,361.582781 C442.875089,365.90941 448.642289,368.481905 454.969751,367.924648 C461.22543,367.376152 466.509403,363.904686 469.384249,359.104914 C472.208321,361.374248 475.746728,362.592152 479.503987,362.257448 C483.149193,361.931505 486.38821,360.208914 488.821849,357.603124 L489.228039,357.631162 C489.781298,356.828571 490.25402,356.006705 490.693475,355.177829 C490.70398,355.162057 490.712734,355.144533 490.721489,355.128762 C490.781016,355.018362 490.854551,354.909714 490.910577,354.797562"/>
<path fill="#04A53C" d="M281.745403,61.5471238 L43.8020066,85.0272762 C50.8770689,71.2921143 61.4905377,58.1685333 71.7380852,51.4148571 L302.004138,21.2896762 C292.894623,32.030019 286.539148,46.6080762 281.745403,61.5471238 Z M302.004138,331.0528 L71.7380852,300.927619 C61.4905377,294.175695 50.8770689,281.050362 43.8020066,267.316952 L281.745403,290.797105 C286.539148,305.7344 292.894623,320.31421 302.004138,331.0528 Z M33.8766098,243.950705 C26.3585902,221.21181 24.03,207.9936 22.1723803,189.066133 L266.684852,192.017143 C267.285384,212.923048 270.116459,239.981562 275.03101,263.034133 L33.8766098,243.950705 Z M33.8766098,108.391771 L275.03101,89.3083429 C270.116459,112.360914 267.285384,139.419429 266.684852,160.327086 L22.1723803,163.276343 C24.03,144.350629 26.3585902,131.130667 33.8766098,108.391771 Z M378.597246,25.7126857 C363.342354,7.93478095 352.390977,-0.411809524 343.010085,0.672914286 C341.261016,0.895466667 76.1168852,37.8952381 76.1168852,37.8952381 C34.0429377,42.1780571 0.416695082,103.32739 0,176.172114 C0.416695082,249.015086 34.0429377,310.164419 76.1168852,314.44899 C76.1168852,314.44899 341.758249,351.583695 343.010085,351.669562 C345.228374,351.655543 347.418649,351.357638 349.57741,350.803886 C347.476426,350.178286 345.538269,349.083048 343.890748,347.590019 L343.621121,347.570743 C313.275915,314.722362 304.885987,278.101105 303.565869,264.667352 C303.56937,264.656838 303.576374,264.648076 303.579875,264.637562 C303.576374,264.648076 303.56937,264.656838 303.565869,264.667352 C294.696216,207.778057 296.161652,177.715962 296.161652,177.715962 C298.061292,95.2962286 322.033515,29.0001524 349.70522,29.638019 C377.375174,30.2776381 398.267705,97.6111238 396.368066,180.030857 C395.814807,203.999924 393.384669,226.586362 389.550374,246.577524 C393.489718,242.226362 398.773692,240.393371 403.68299,240.740343 C404.586413,240.805181 405.465325,240.957638 406.326728,241.155657 C423.131095,149.125867 405.514348,59.262019 378.597246,25.7126857 Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560"><g fill="none"><path fill="#FFF" d="M370.57 474.214l23.466-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278c-6.748 10.248-19.863 20.86-33.59 27.936zm-78.197 21.631l2.947-244.528c20.894-.599 47.933-3.43 70.97-8.346l-19.07 241.17c-22.724 7.518-35.934 9.848-54.847 11.704zm-99.694-252.874c23.038 4.916 50.077 7.747 70.971 8.346l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705l-19.072-241.17zm-67.974-26.975c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.59-27.936l-30.104-230.282z"/><path fill="gold" d="M118.905 157.445c1.357 28.827 72.771 51.677 160.578 51.176 76.687-.438 140.659-18.546 156.329-42.336a22.976 22.976 0 00-14.058-7.426c.06-.7.098-1.406.095-2.122-.065-11.4-8.429-20.788-19.327-22.54.287-1.474.438-2.999.43-4.559-.072-12.696-10.426-22.928-23.124-22.856-.287.001-.568.036-.853.049a22.911 22.911 0 001.254-7.56c-.074-12.697-10.425-22.93-23.123-22.858a22.914 22.914 0 00-8.247 1.6c-3.632-6.835-10.606-11.6-18.737-12.149-1.416-11.4-11.157-20.195-22.93-20.129-7.41.042-13.963 3.6-18.136 9.065-4.233-4.605-10.3-7.494-17.047-7.456-12.698.072-22.932 10.424-22.86 23.118a22.983 22.983 0 001.115 6.946 22.918 22.918 0 00-13.07 7.459c-2.644-9.847-11.637-17.084-22.314-17.024-9.975.057-18.406 6.47-21.537 15.366-8.474 3.426-14.439 11.738-14.383 21.433.012 2.154.342 4.227.907 6.202a22.876 22.876 0 00-9.328-1.932c-10.012.058-18.47 6.516-21.574 15.465a22.83 22.83 0 00-9.788-2.149c-12.698.072-22.934 10.422-22.86 23.118a22.833 22.833 0 003.159 11.463c-.202.203-.379.426-.571.636"/><path fill="#FA320A" d="M404.161 446.278c-6.749 10.248-19.864 20.86-33.59 27.936l23.465-237.956c14.93-4.796 29.498-11.15 40.23-20.262L404.16 446.278zM347.22 484.14c-22.723 7.519-35.934 9.85-54.847 11.705l2.947-244.528c20.894-.599 47.933-3.43 70.973-8.346L347.22 484.14zm-135.47 0l-19.07-241.17c23.037 4.917 50.076 7.748 70.97 8.347l2.948 244.528c-18.914-1.856-32.123-4.186-54.847-11.705zm-56.94-37.862l-30.105-230.282c10.732 9.112 25.3 15.466 40.23 20.262l23.464 237.956c-13.726-7.075-26.84-17.688-33.588-27.936zm247.668-321.143c.298 1.453.465 2.955.473 4.498a23.018 23.018 0 01-.43 4.56c10.9 1.749 19.263 11.137 19.328 22.54a23.59 23.59 0 01-.095 2.12 22.976 22.976 0 0114.058 7.425c-15.669 23.792-79.642 41.9-156.327 42.34-87.807.502-159.221-22.346-160.58-51.175.192-.208.37-.433.57-.634-1.355-2.311-2.29-4.887-2.773-7.62-8.408 7.979-13.495 14.412-12.6 23.78.085 1.251 37.196 266.911 37.196 266.911 4.282 42.075 65.391 75.703 138.187 76.12 72.796-.417 133.907-34.045 138.187-76.12 0 0 37.11-265.66 37.197-266.912 1.777-18.736-20.15-35.745-52.39-47.833z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 190.24 81.52"><defs><linearGradient id="a" y1="40.76" x2="190.24" y2="40.76" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#90cea1"/><stop offset=".56" stop-color="#3cbec9"/><stop offset="1" stop-color="#00b3e5"/></linearGradient></defs><g data-name="Layer 2"><path d="M105.67 36.06h66.9a17.67 17.67 0 0017.67-17.66A17.67 17.67 0 00172.57.73h-66.9A17.67 17.67 0 0088 18.4a17.67 17.67 0 0017.67 17.66zm-88 45h76.9a17.67 17.67 0 0017.67-17.66 17.67 17.67 0 00-17.67-17.67h-76.9A17.67 17.67 0 000 63.4a17.67 17.67 0 0017.67 17.66zm-7.26-45.64h7.8V6.92h10.1V0h-28v6.9h10.1zm28.1 0h7.8V8.25h.1l9 27.15h6l9.3-27.15h.1V35.4h7.8V0H66.76l-8.2 23.1h-.1L50.31 0h-11.8zm113.92 20.25a15.07 15.07 0 00-4.52-5.52 18.57 18.57 0 00-6.68-3.08 33.54 33.54 0 00-8.07-1h-11.7v35.4h12.75a24.58 24.58 0 007.55-1.15 19.34 19.34 0 006.35-3.32 16.27 16.27 0 004.37-5.5 16.91 16.91 0 001.63-7.58 18.5 18.5 0 00-1.68-8.25zM145 68.6a8.8 8.8 0 01-2.64 3.4 10.7 10.7 0 01-4 1.82 21.57 21.57 0 01-5 .55h-4.05v-21h4.6a17 17 0 014.67.63 11.66 11.66 0 013.88 1.87A9.14 9.14 0 01145 59a9.87 9.87 0 011 4.52 11.89 11.89 0 01-1 5.08zm44.63-.13a8 8 0 00-1.58-2.62 8.38 8.38 0 00-2.42-1.85 10.31 10.31 0 00-3.17-1v-.1a9.22 9.22 0 004.42-2.82 7.43 7.43 0 001.68-5 8.42 8.42 0 00-1.15-4.65 8.09 8.09 0 00-3-2.72 12.56 12.56 0 00-4.18-1.3 32.84 32.84 0 00-4.62-.33h-13.2v35.4h14.5a22.41 22.41 0 004.72-.5 13.53 13.53 0 004.28-1.65 9.42 9.42 0 003.1-3 8.52 8.52 0 001.2-4.68 9.39 9.39 0 00-.55-3.18zm-19.42-15.75h5.3a10 10 0 011.85.18 6.18 6.18 0 011.7.57 3.39 3.39 0 011.22 1.13 3.22 3.22 0 01.48 1.82 3.63 3.63 0 01-.43 1.8 3.4 3.4 0 01-1.12 1.2 4.92 4.92 0 01-1.58.65 7.51 7.51 0 01-1.77.2h-5.65zm11.72 20a3.9 3.9 0 01-1.22 1.3 4.64 4.64 0 01-1.68.7 8.18 8.18 0 01-1.82.2h-7v-8h5.9a15.35 15.35 0 012 .15 8.47 8.47 0 012.05.55 4 4 0 011.57 1.18 3.11 3.11 0 01.63 2 3.71 3.71 0 01-.43 1.92z" fill="url(#a)" data-name="Layer 1"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560"><g fill="none"><path fill="#FA320A" d="M478.29 296.976c-3.99-63.966-36.52-111.823-85.468-138.579.278 1.56-1.109 3.508-2.688 2.818-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.732-31.535 4.367-43.586-24.338 21.81-38.472 30.017-85.138 19.186-29.878 31.241-46.809 74-43.485 127.265 6.78 108.735 108.63 170.89 211.193 164.49 102.556-6.395 193.466-80.572 186.683-189.307"/><path fill="#00912D" d="M291.375 132.293c21.075-5.023 81.693-.49 101.114 25.274 1.166 1.545-.475 4.468-2.355 3.648-32.016-14.006-86.328 31.32-124.282 7.584.285 8.519-1.378 50.072-59.914 52.483-1.382.056-2.142-1.355-1.268-2.354 7.828-8.929 15.73-31.535 4.367-43.586-26.512 23.758-40.884 31.392-98.426 15.838-1.883-.508-1.241-3.535.762-4.298 10.876-4.157 35.515-22.361 58.824-30.385 4.438-1.526 8.862-2.71 13.18-3.4-25.665-2.293-37.235-5.862-53.559-3.4-1.789.27-3.004-1.813-1.895-3.241 21.995-28.332 62.513-36.888 87.512-21.837-15.41-19.094-27.48-34.321-27.48-34.321l28.601-16.246s11.817 26.4 20.414 45.614c21.275-31.435 60.86-34.336 77.585-12.033.992 1.326-.045 3.21-1.702 3.171-13.612-.331-21.107 12.05-21.675 21.466l.197.023"/></g></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="560" height="560"><path fill="#0AC855" d="M445.185 444.684c-79.369 4.167-95.587-86.652-126.726-86.006-13.268.279-23.726 14.151-19.133 30.32 2.525 8.888 9.53 21.923 13.944 30.011 15.57 28.544-7.447 60.845-34.383 63.577-44.76 4.54-63.433-21.426-62.278-48.007 1.3-29.84 26.6-60.331.65-73.305-27.194-13.597-49.301 39.572-75.325 51.439-23.553 10.741-56.248 2.413-67.872-23.741-8.164-18.379-6.68-53.768 29.67-67.27 22.706-8.433 73.305 11.029 75.9-13.623 2.992-28.416-53.155-30.812-70.06-37.626-29.912-12.055-47.567-37.85-33.734-65.522 10.378-20.757 40.915-29.203 64.223-20.11 27.922 10.892 32.404 39.853 46.71 51.897 12.324 10.38 29.19 11.68 40.22 4.543 8.135-5.265 10.843-16.828 7.774-27.39-4.07-14.023-14.875-22.773-25.415-31.346-18.758-15.249-45.24-28.36-29.222-69.983 13.13-34.11 51.642-35.34 51.642-35.34 15.3-1.72 29.002 2.9 40.167 12.875 14.927 13.335 17.834 31.16 15.336 50.176-2.283 17.358-8.426 32.56-11.63 49.759-3.717 19.966 6.954 40.086 27.249 40.869 26.694 1.031 34.698-19.486 37.964-32.492 4.782-19.028 11.058-36.694 28.718-47.82 25.346-15.97 60.552-12.47 76.886 18.222 12.92 24.284 8.772 57.715-11.047 75.97-8.892 8.188-19.584 11.075-31.148 11.156-16.585.117-33.162-.29-48.556 7.471-10.48 5.281-15.047 13.888-15.045 25.423 0 11.242 5.853 18.585 15.336 23.363 17.86 9.003 37.577 10.843 56.871 14.222 27.98 4.9 52.581 14.755 68.375 40.72.142.228.28.458.415.69 18.139 30.741-.831 75.005-36.476 76.878"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -15,9 +15,9 @@
<h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp; <h3><span id="sessions-xml">Activity</span> &nbsp;&nbsp;
<small> <small>
<span id="currentActivityHeader" style="display: none;"> <span id="currentActivityHeader" style="display: none;">
Streams: <span id="currentActivityHeader-streams"></span> | Sessions: <span id="currentActivityHeader-streams"></span> |
Bandwidth: <span id="currentActivityHeader-bandwidth"></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 id="currentActivityHeader-bandwidth-tooltip" data-toggle="tooltip" title="Streaming Brain Estimate (Reserved Bandwidth)"><i class="fa fa-info-circle"></i></span>
</span> </span>
</small> </small>
</h3> </h3>
@@ -27,6 +27,8 @@
<div class="text-muted" id="dashboard-checking-activity"><i class="fa fa-refresh fa-spin"></i> Checking for activity...</div> <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']: % elif config['pms_is_cloud']:
<div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div> <div id="dashboard-no-activity" class="text-muted">Plex Cloud server is sleeping.</div>
% elif not config['first_run_complete']:
<div id="dashboard-no-activity" class="text-muted">The Tautulli setup wizard has not been completed. Please click <a href="welcome">here</a> to go to the setup wizard.</div>
% else: % else:
<div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server. <div id="dashboard-no-activity" class="text-muted">There was an error communicating with your Plex Server.
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -177,10 +179,10 @@
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button> <button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">Terminate Session</h4> <h4 class="modal-title">Terminate Stream</h4>
</div> </div>
<div class="modal-body" style="text-align: center;"> <div class="modal-body" style="text-align: center;">
<p>Are you sure you want to terminate this session?</p> <p>Are you sure you want to terminate this stream?</p>
<p> <p>
<strong> <strong>
<span id="terminate-user"></span><br /> <span id="terminate-user"></span><br />
@@ -332,13 +334,13 @@
streams_header = streams_header.replace(/, $/, '') + ')'; streams_header = streams_header.replace(/, $/, '') + ')';
$('#currentActivityHeader-streams').text(streams_header); $('#currentActivityHeader-streams').text(streams_header);
var bandwidth_header = ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps')); var bandwidth_header = ((total_bw > 1000000) ? ((total_bw / 1000000).toFixed(1) + ' Gbps') : ((total_bw > 1000) ? ((total_bw / 1000).toFixed(1) + ' Mbps') : (total_bw + ' kbps')));
var lan_wan_bandwidth_header = ''; var lan_wan_bandwidth_header = '';
if (lan_bw) { if (lan_bw) {
lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps')) + ', '; lan_wan_bandwidth_header += 'LAN: ' + ((lan_bw > 1000000) ? ((lan_bw / 1000000).toFixed(1) + ' Gbps') : ((lan_bw > 1000) ? ((lan_bw / 1000).toFixed(1) + ' Mbps') : (lan_bw + ' kbps'))) + ', ';
} }
if (wan_bw) { if (wan_bw) {
lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps')) + ', '; lan_wan_bandwidth_header += 'WAN: ' + ((wan_bw > 1000000) ? ((wan_bw / 1000000).toFixed(1) + ' Gbps') : ((wan_bw > 1000) ? ((wan_bw / 1000).toFixed(1) + ' Mbps') : (wan_bw + ' kbps'))) + ', ';
} }
if (lan_wan_bandwidth_header) { if (lan_wan_bandwidth_header) {
bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')'; bandwidth_header += ' (' + lan_wan_bandwidth_header.replace(/, $/, '') + ')';
@@ -353,8 +355,11 @@
var session_id = s.session_id; var session_id = s.session_id;
var instance = $('#activity-instance-' + key); var instance = $('#activity-instance-' + key);
// Create a new instance if it doesn't exist // Create a new instance if it doesn't exist or recreate the entire instance
if (!(instance.length)) { // if the rating key changed (for movies or episodes) of guid changed (for live tv) with the same session key
if (!(instance.length) ||
(s.media_type !== 'track' && s.rating_key !== instance.data('rating_key').toString()) ||
(s.live === 1 && s.guid !== instance.data('guid'))) {
create_instances.push(key); create_instances.push(key);
getActivityInstance(key); getActivityInstance(key);
return; return;
@@ -372,6 +377,9 @@
case 'buffering': case 'buffering':
state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;'; state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;';
break; break;
case 'error':
state_icon = '<i class="fa fa-fw fa-exclamation-triangle"></i>&nbsp;';
break;
default: default:
state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;'; state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;';
} }
@@ -380,33 +388,33 @@
// Switching tracks can be under the same session key, so need to update the info. // Switching tracks can be under the same session key, so need to update the info.
if (s.media_type === 'track') { if (s.media_type === 'track') {
// Update if artist changed // Update if artist changed
if (s.grandparent_rating_key !== instance.data('grandparent_rating_key')) { if (s.grandparent_rating_key !== instance.data('grandparent_rating_key').toString()) {
$('#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)'); $('#background-' + key).css('background-image', 'url(' + page('pms_image_proxy', s.art, s.rating_key, 500, 280, 40, '282828', 3, 'art', true) + ')');
$('#metadata-grandparent_title-' + key) $('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key) .attr('href', page('info', s.grandparent_rating_key))
.attr('title', s.original_title || s.grandparent_title) .attr('title', s.original_title || s.grandparent_title)
.text(s.original_title || s.grandparent_title); .text(s.original_title || s.grandparent_title);
} }
// Update cover if album changed // 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').toString()) {
$('#poster-' + key).css('background-image', 'url(pms_image_proxy?img=' + s.parent_thumb + '&width=300&height=300&fallback=poster&refresh=true)'); $('#poster-' + key).css('background-image', 'url(' + page('pms_image_proxy', s.parent_thumb, s.parent_rating_key, 300, 300, null, null, null, 'poster', 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-' + key + '-bg').css('background-image', 'url(' + page('pms_image_proxy', s.parent_thumb, s.parent_rating_key, 300, 300, 60, '282828', 3, 'poster', true) + ')');
$('#poster-url-' + key) $('#poster-url-' + key)
.attr('href', 'info?rating_key=' + s.parent_rating_key) .attr('href', page('info', s.parent_rating_key))
.attr('title', s.parent_title); .attr('title', s.parent_title);
$('#metadata-parent_title-' + key) $('#metadata-parent_title-' + key)
.attr('href', 'info?rating_key=' + s.parent_rating_key) .attr('href', page('info', s.parent_rating_key))
.attr('title', s.parent_title) .attr('title', s.parent_title)
.text(s.parent_title); .text(s.parent_title);
} }
// Update cover if track changed // Update cover if track changed
if (s.rating_key !== instance.data('rating_key')) { if (s.rating_key !== instance.data('rating_key').toString()) {
$('#metadata-grandparent_title-' + key) $('#metadata-grandparent_title-' + key)
.attr('href', 'info?rating_key=' + s.grandparent_rating_key) .attr('href', page('info', s.grandparent_rating_key))
.attr('title', s.original_title || s.grandparent_title) .attr('title', s.original_title || s.grandparent_title)
.text(s.original_title || s.grandparent_title); .text(s.original_title || s.grandparent_title);
$('#metadata-title-' + key) $('#metadata-title-' + key)
.attr('href', 'info?rating_key=' + s.rating_key) .attr('href', page('info', s.rating_key))
.attr('title', s.title) .attr('title', s.title)
.text(s.title); .text(s.title);
} }
@@ -426,14 +434,16 @@
var transcode_container = ''; var transcode_container = '';
if (s.stream_container_decision === 'transcode') { if (s.stream_container_decision === 'transcode') {
transcode_container = 'Transcode (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')'; transcode_container = 'Converting (' + s.container.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_container.toUpperCase() + ')';
} else { } else {
transcode_container = 'Direct Play (' + s.container.toUpperCase() + ')'; transcode_container = 'Direct Play (' + s.stream_container.toUpperCase() + ')';
} }
$('#transcode_container-' + key).html(transcode_container); $('#transcode_container-' + key).html(transcode_container);
var video_decision = ''; var video_decision = '';
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) { if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.stream_video_decision) {
var v_bd = (s.video_dynamic_range === 'HDR') ? ' ' + s.video_dynamic_range : '';
var sv_bd = (s.video_dynamic_range === 'HDR') ? ' ' + s.stream_video_dynamic_range : '';
var v_res= ''; var v_res= '';
switch (s.video_resolution.toLowerCase()) { switch (s.video_resolution.toLowerCase()) {
case 'sd': case 'sd':
@@ -443,7 +453,7 @@
v_res = '4k'; v_res = '4k';
break; break;
default: default:
v_res = s.video_resolution + 'p' v_res = s.video_full_resolution;
} }
var sv_res = ''; var sv_res = '';
switch (s.stream_video_resolution.toLowerCase()) { switch (s.stream_video_resolution.toLowerCase()) {
@@ -454,16 +464,16 @@
sv_res = '4k'; sv_res = '4k';
break; break;
default: default:
sv_res = s.stream_video_resolution + 'p' sv_res = s.stream_video_full_resolution;
} }
if (s.stream_video_decision === 'transcode') { if (s.stream_video_decision === 'transcode') {
var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : ''; var hw_d = (s.transcode_hw_decoding === 1) ? ' (HW)' : '';
var hw_e = (s.transcode_hw_encoding === 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 + ')'; video_decision = 'Transcode (' + s.video_codec.toUpperCase() + hw_d + ' ' + v_res + v_bd + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_video_codec.toUpperCase() + hw_e + ' ' + sv_res + sv_bd + ')';
} else if (s.stream_video_decision === 'copy') { } else if (s.stream_video_decision === 'copy') {
video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + ')'; video_decision = 'Direct Stream (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')';
} else { } else {
video_decision = 'Direct Play (' + s.video_codec.toUpperCase() + ' ' + v_res + ')'; video_decision = 'Direct Play (' + s.stream_video_codec.toUpperCase() + ' ' + sv_res + sv_bd + ')';
} }
} else if (s.media_type === 'photo') { } else if (s.media_type === 'photo') {
video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')'; video_decision = 'Direct Play (' + s.width + 'x' + s.height + ')';
@@ -479,21 +489,22 @@
} else if (s.stream_audio_decision === 'copy') { } else if (s.stream_audio_decision === 'copy') {
audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')'; audio_decision = 'Direct Stream (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
} else { } else {
audio_decision = 'Direct Play (' + a_codec + ' ' + capitalizeFirstLetter(s.audio_channel_layout.split('(')[0]) + ')'; audio_decision = 'Direct Play (' + sa_codec + ' ' + capitalizeFirstLetter(s.stream_audio_channel_layout.split('(')[0]) + ')';
} }
} }
$('#audio_decision-' + key).html(audio_decision); $('#audio_decision-' + key).html(audio_decision);
var subtitle_decision = 'None'; var subtitle_decision = 'None';
if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) { if (['movie', 'episode', 'clip'].indexOf(s.media_type) > -1 && s.subtitles === 1) {
var subtitle_codec = (s.stream_subtitle_codec && s.stream_subtitle_transient) ? 'None' : s.subtitle_codec.toUpperCase();
if (s.stream_subtitle_decision === 'transcode') { if (s.stream_subtitle_decision === 'transcode') {
subtitle_decision = 'Transcode (' + s.subtitle_codec.toUpperCase() + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')'; subtitle_decision = 'Transcode (' + subtitle_codec + ' <i class="fa fa-long-arrow-right"></i> ' + s.stream_subtitle_codec.toUpperCase() + ')';
} else if (s.stream_subtitle_decision === 'copy') { } else if (s.stream_subtitle_decision === 'copy') {
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')'; subtitle_decision = 'Direct Stream (' + subtitle_codec + ')';
} else if (s.stream_subtitle_decision === 'burn') { } else if (s.stream_subtitle_decision === 'burn') {
subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')'; subtitle_decision = 'Burn (' + subtitle_codec + ')';
} else { } else {
subtitle_decision = 'Direct Play (' + ((s.synced_version === '1') ? s.stream_subtitle_codec.toUpperCase() : s.subtitle_codec.toUpperCase()) + ')'; subtitle_decision = 'Direct Play (' + ((s.synced_version === '1') ? subtitle_codec : s.stream_subtitle_codec.toUpperCase()) + ')';
} }
} }
$('#subtitle_decision-' + key).html(subtitle_decision); $('#subtitle_decision-' + key).html(subtitle_decision);
@@ -519,7 +530,9 @@
if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') { if (s.media_type !== 'photo' && s.bandwidth !== 'Unknown') {
var bw = parseInt(s.bandwidth) || 0; var bw = parseInt(s.bandwidth) || 0;
if (bw > 1000) { if (bw > 1000000) {
bw = (bw / 1000000).toFixed(1) + ' Gbps';
} else if (bw > 1000) {
bw = (bw / 1000).toFixed(1) + ' Mbps'; bw = (bw / 1000).toFixed(1) + ' Mbps';
} else { } else {
bw = bw + ' kbps' bw = bw + ' kbps'
@@ -538,10 +551,12 @@
// Update the progress bars, percent - 3 because of 3px padding-right // Update the progress bars, percent - 3 because of 3px padding-right
$('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%') $('#buffer-bar-' + key).width(parseInt(s.transcode_progress) - 3 + '%').html(s.transcode_progress + '%')
.attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%'); .attr('data-original-title', 'Transcoder Progress ' + s.transcode_progress + '%');
var progress_bar = $('#progress-bar-' + key); if (s.live !== 1) {
progress_bar.data('state', s.state); var progress_bar = $('#progress-bar-' + key);
if (progress_bar.data('last_view_offset') !== s.view_offset) { progress_bar.data('state', s.state);
progress_bar.data('last_view_offset', s.view_offset).data('view_offset', s.view_offset); if (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 // Add temporary class so we know which instances are still active
@@ -554,6 +569,7 @@
$(instance).removeClass('updated-temp'); $(instance).removeClass('updated-temp');
} else { } else {
$(instance).find('[data-toggle=tooltip]').tooltip('destroy'); $(instance).find('[data-toggle=tooltip]').tooltip('destroy');
$(instance).find('[data-toggle=popover]').popover('destroy');
$(instance).remove(); $(instance).remove();
} }
}); });
@@ -578,9 +594,27 @@
session_key: session_key session_key: session_key
}, },
complete: function(xhr, status) { complete: function(xhr, status) {
$('#currentActivity').append(xhr.responseText); var instance = $('#activity-instance-' + session_key);
if (instance.length) {
instance.replaceWith(xhr.responseText);
} else {
$('#currentActivity').append(xhr.responseText);
}
$('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar(); $('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller').scrollbar();
$('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 }); $('#activity-instance-' + session_key + ' [data-toggle=tooltip]').tooltip({ container: 'body', placement: 'right', delay: 50 });
$('#activity-instance-' + session_key + ' [data-toggle=popover]').popover({
html: true,
container: 'body',
trigger: 'hover',
placement: 'right',
delay: 50,
template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
content: function () {
return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />';
}
});
$('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 }); $('#terminate-button-' + session_key).tooltip('destroy').tooltip({ container: 'body', placement: 'left', delay: 50 });
lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller'); lockScroll('#activity-instance-' + session_key + ' .dashboard-activity-info-scroller');
@@ -696,6 +730,88 @@
% endif % endif
</script> </script>
% endif % endif
% if 'watch_stats' in config['home_sections'] or 'library_stats' in config['home_sections']:
<script>
function statsCardCallback() {
$('.dashboard-stats-instance .dashboard-stats-info-scroller').scrollbar();
function changeImages(elem) {
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 guid = $(elem).data('guid');
var live = $(elem).data('live');
var [height, fallback_poster, fallback_art] = [450, 'poster', 'art'];
if ($.inArray(stat_id, ['top_music', 'popular_music']) > -1) {
[height, fallback_poster, fallback_art] = [300, 'cover', 'art'];
} else if (live) {
[height, fallback_poster, fallback_art] = [450, 'poster-live', 'art-live'];
}
var href = '#';
if (stat_id === 'most_concurrent') {
return
} else if (stat_id === 'top_users') {
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + (user_thumb || 'images/gravatar-default.png') + ')');
if (user_id) {
href = page('user', user_id);
}
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('user'));
} 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') + '-rgba');
} else {
if (rating_key) {
if (live) {
href = page('info', rating_key, guid, true, live);
} else {
href = page('info', rating_key);
}
}
$('#stats-thumb-url-' + stat_id).attr('href', href).prop('title', $(elem).data('title'));
$('#stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art, rating_key, 500, 280, 40, '282828', 3, fallback_art) + ')');
$('#stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', thumb, rating_key, 300, height, null, null, null, fallback_poster) + ')');
$('#stats-thumb-' + stat_id + '-bg').css('background-image', 'url(' + page('pms_image_proxy', thumb, rating_key, 300, height, 60, '282828', 3, fallback_poster) + ')');
$('#library-stats-background-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', art, rating_key, 500, 280, 40, '282828', 3, fallback_art) + ')');
if (thumb.startsWith('http')) {
$('#library-stats-thumb-' + stat_id).css('background-image', 'url(' + page('pms_image_proxy', thumb, rating_key, 300, 300, null, null, null, 'cover') + ')')
.removeClass('svg-icon library-' + stat_id);
} else {
$('#library-stats-thumb-' + stat_id).css('background-image', '')
.addClass('svg-icon library-' + stat_id);
}
}
}
$('.dashboard-stats-info-item').mouseenter(function () {
changeImages(this);
if ($(this).data('stat_id') === 'last_watched') {
var friendly_name = $(this).data('friendly_name');
var last_watch = moment($(this).data('last_watch'), 'X').format(date_format);
$('#last-watched-header-info').html(friendly_name);
} else if ($(this).data('stat_id') === 'most_concurrent') {
var started = moment($(this).data('started'), 'X').format(date_format + ' ' + time_format);
$('#most-concurrent-header-info').html(started);
}
});
$('.dashboard-stats-instance').mouseleave(function () {
changeImages($(this).find('.dashboard-stats-info-item').first());
if ($(this).data('stat_id') === 'last_watched') {
$('#last-watched-header-info').text($(this).find('.dashboard-stats-info-item').first().data('friendly_name'));
} else if ($(this).data('stat_id') === 'most_concurrent') {
$('#most-concurrent-header-info').text('streams');
}
});
}
</script>
% endif
% if 'watch_stats' in config['home_sections']: % if 'watch_stats' in config['home_sections']:
<script> <script>
function getHomeStats(time_range, stats_type) { function getHomeStats(time_range, stats_type) {
@@ -714,6 +830,7 @@
$("#home-stats").html(xhr.responseText); $("#home-stats").html(xhr.responseText);
$('#ajaxMsg').fadeOut(); $('#ajaxMsg').fadeOut();
lockScroll('#home-stats .dashboard-stats-info-scroller'); lockScroll('#home-stats .dashboard-stats-info-scroller');
statsCardCallback();
} }
}); });
} }
@@ -753,6 +870,7 @@
data: { }, data: { },
complete: function (xhr, status) { complete: function (xhr, status) {
$("#library-stats").html(xhr.responseText); $("#library-stats").html(xhr.responseText);
statsCardCallback();
} }
}); });
} }
@@ -871,7 +989,10 @@
async: true, async: true,
complete: function (xhr, status) { complete: function (xhr, status) {
$("#changelog-modal .modal-body").html(xhr.responseText); $("#changelog-modal .modal-body").html(xhr.responseText);
$('#changelog-modal').modal(); $('#changelog-modal').modal({
backdrop: 'static',
keyboard: false
});
} }
}); });
</script> </script>

View File

@@ -36,25 +36,34 @@ DOCUMENTATION :: END
</%doc> </%doc>
<%! <%!
from collections import defaultdict
import re import re
from plexpy import notifiers from plexpy import notifiers
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
from plexpy.helpers import page, get_percent
# Get audio codec file # Get audio codec file
def af(codec): def af(codec):
for pattern, file_type in MEDIA_FLAGS_AUDIO.iteritems(): for pattern, file_type in MEDIA_FLAGS_AUDIO.items():
if re.match(pattern, codec): if re.match(pattern, codec):
return file_type return file_type
return codec return codec
# Get audio codec file # Get video codec file
def vf(codec): def vf(codec):
for pattern, file_type in MEDIA_FLAGS_VIDEO.iteritems(): for pattern, file_type in MEDIA_FLAGS_VIDEO.items():
if re.match(pattern, codec): if re.match(pattern, codec):
return file_type return file_type
return codec return codec
# Get video resolution file
def vr(resolution):
if resolution in ('1080i', '576i', '480i'):
return resolution
else:
return resolution.lower().rstrip('ip')
def br(text): def br(text):
return text.replace('\n', '<br /><br />') return text.replace('\n', '<br /><br />')
%> %>
@@ -68,11 +77,15 @@ DOCUMENTATION :: END
</%def> </%def>
<%def name="body()"> <%def name="body()">
% if data: % if metadata:
<% media_info = data['media_info'][0] if data['media_info'] else {} %> <%
data = defaultdict(lambda: None, **metadata)
media_info = defaultdict(lambda: None, **(data['media_info'][0] if data['media_info'] else {}))
%>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['art']}&width=1920&height=1080)"></div> <% fallback = 'art-live-full' if data['live'] else None %>
<div class="art-face" style="background-image:url(${page('pms_image_proxy', data['art'], data['rating_key'], 1920, 1080, fallback=fallback)})"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -81,44 +94,60 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="summary-navbar-list"> <div class="summary-navbar-list">
<ul class="list-unstyled breadcrumb"> <ul class="list-unstyled breadcrumb">
% if data['media_type'] in ('movie', 'collection'): % if data['live']:
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
% if data['media_type'] == 'movie':
<li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm">${data['grandparent_title']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
% if data['media_index']:
<li>Season ${data['parent_media_index']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
% else:
<li class="active metadata-xml">${data['title']}</li>
% endif
% endif
% elif data['media_type'] in ('movie', 'collection'):
<li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li> <li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'show': % elif data['media_type'] == 'show':
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li> <li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'season': % elif data['media_type'] == 'season':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Season ${data['media_index']}</li> <li class="active metadata-xml">Season ${data['media_index']}</li>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="${page('info', data['grandparent_rating_key'])}">${data['grandparent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">Season ${data['parent_media_index']}</a></li> <li><a href="${page('info', data['parent_rating_key'])}">Season ${data['parent_media_index']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li> <li class="active metadata-xml">Episode ${data['media_index']} - ${data['title']}</li>
% elif data['media_type'] == 'artist': % elif data['media_type'] == 'artist':
<li><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li> <li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'album': % elif data['media_type'] == 'album':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">${data['title']}</li> <li class="active metadata-xml">${data['title']}</li>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<li class="hidden-xs hidden-sm"><a href="library?section_id=${data['section_id']}">${data['library_name']}</a></li> <li class="hidden-xs hidden-sm"><a href="${page('library', data['section_id'])}">${data['library_name']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm"><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></li> <li class="hidden-xs hidden-sm"><a href="${page('info', data['grandparent_rating_key'])}">${data['grandparent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></li> <li><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span> <span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li> <li class="active metadata-xml">Track ${data['media_index']} - ${data['title']}</li>
% endif % endif
@@ -131,11 +160,18 @@ DOCUMENTATION :: END
<div class="summary-content-poster hidden-xs hidden-sm"> <div class="summary-content-poster hidden-xs hidden-sm">
% if data['media_type'] == 'track': % if data['media_type'] == 'track':
<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"> <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: % elif not data['live']:
<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"> <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 % endif
% if data['live']:
<div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['grandparent_thumb'] or data['thumb'], data['rating_key'], 300, 450, fallback='poster-live')});">
<div class="summary-poster-face-overlay">
<span></span>
</div>
</div>
% else:
% if data['media_type'] == 'episode': % if data['media_type'] == 'episode':
<div class="summary-poster-face-episode" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=280&fallback=art);"> <div class="summary-poster-face-episode" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 280, fallback='art')});">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
</div> </div>
@@ -144,7 +180,7 @@ DOCUMENTATION :: END
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
% elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track': % elif data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<div class="summary-poster-face-track" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=500&height=500&fallback=cover);"> <div class="summary-poster-face-track" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 500, 500, fallback='cover')});">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
</div> </div>
@@ -153,7 +189,7 @@ DOCUMENTATION :: END
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
% else: % else:
<div class="summary-poster-face" style="background-image: url(pms_image_proxy?img=${data['thumb']}&width=300&height=450&fallback=poster);"> <div class="summary-poster-face" style="background-image: url(${page('pms_image_proxy', data['thumb'], data['rating_key'], 300, 450, fallback='poster')});">
<div class="summary-poster-face-overlay"> <div class="summary-poster-face-overlay">
<span></span> <span></span>
</div> </div>
@@ -162,24 +198,37 @@ DOCUMENTATION :: END
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
% endif % endif
% endif
% if not data['live']:
</a> </a>
% endif
</div> </div>
<div class="summary-content-title"> <div class="summary-content-title">
% if data['media_type'] in ('movie', 'show', 'artist', 'collection'): % if data['live']:
% if data['media_type'] == 'movie':
<h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['media_type'] == 'episode':
<h1>${data['grandparent_title']}</h1>
<h2>${data['title']}</h2>
% if data['media_index']:
<h3 class="hidden-xs">S${data['parent_media_index']} &middot; E${data['media_index']}</h3>
% endif
% endif
% elif data['media_type'] in ('movie', 'show', 'artist', 'collection'):
<h1>&nbsp;</h1><h1>${data['title']}</h1> <h1>&nbsp;</h1><h1>${data['title']}</h1>
% elif data['media_type'] == 'season': % elif data['media_type'] == 'season':
<h1>&nbsp;</h1><h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1> <h1>&nbsp;</h1><h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
<h3 class="hidden-xs">S${data['media_index']}</h3> <h3 class="hidden-xs">S${data['media_index']}</h3>
% elif data['media_type'] == 'episode': % elif data['media_type'] == 'episode':
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['grandparent_title']}</a></h1> <h1><a href="${page('info', data['grandparent_rating_key'])}">${data['grandparent_title']}</a></h1>
<h2>${data['title']}</h2> <h2>${data['title']}</h2>
<h3 class="hidden-xs">S${data['parent_media_index']} &middot; E${data['media_index']}</h3> <h3 class="hidden-xs">S${data['parent_media_index']} &middot; E${data['media_index']}</h3>
% elif data['media_type'] == 'album': % elif data['media_type'] == 'album':
<h1><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a></h1> <h1><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a></h1>
<h2>${data['title']}</h2> <h2>${data['title']}</h2>
% elif data['media_type'] == 'track': % elif data['media_type'] == 'track':
<h1><a href="info?rating_key=${data['grandparent_rating_key']}">${data['original_title'] or data['grandparent_title']}</a></h1> <h1><a href="${page('info', data['grandparent_rating_key'])}">${data['original_title'] or data['grandparent_title']}</a></h1>
<h2><a href="info?rating_key=${data['parent_rating_key']}">${data['parent_title']}</a> - ${data['title']}</h2> <h2><a href="${page('info', data['parent_rating_key'])}">${data['parent_title']}</a> - ${data['title']}</h2>
<h3 class="hidden-xs">T${data['media_index']}</h3> <h3 class="hidden-xs">T${data['media_index']}</h3>
% endif % endif
</div> </div>
@@ -187,7 +236,7 @@ DOCUMENTATION :: END
</div> </div>
<div class="summary-content-wrapper"> <div class="summary-content-wrapper">
<div class="col-md-9"> <div class="col-md-9">
% if data['media_type'] == 'movie': % if data['media_type'] == 'movie' or data['live']:
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;"> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 305px;">
% elif data['media_type'] in ('show', 'season', 'collection'): % elif data['media_type'] in ('show', 'season', 'collection'):
<div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;"> <div class="summary-content-padding hidden-xs hidden-sm" style="height: 270px;">
@@ -206,7 +255,7 @@ DOCUMENTATION :: END
<img class="summary-content-media-flag" title="${media_info['video_codec']}" src="${http_root}images/media_flags/video_codec/${media_info['video_codec'] | vf}.png" /> <img class="summary-content-media-flag" title="${media_info['video_codec']}" src="${http_root}images/media_flags/video_codec/${media_info['video_codec'] | vf}.png" />
% endif % endif
% if data['media_type'] != 'track' and media_info['video_resolution']: % if data['media_type'] != 'track' and media_info['video_resolution']:
<img class="summary-content-media-flag" title="${media_info['video_resolution']}" src="${http_root}images/media_flags/video_resolution/${media_info['video_resolution']}.png" /> <img class="summary-content-media-flag" title="${media_info['video_resolution']}" src="${http_root}images/media_flags/video_resolution/${media_info['video_full_resolution'] | vr}.png" />
% endif % endif
% if media_info['audio_codec']: % if media_info['audio_codec']:
<img class="summary-content-media-flag" title="${media_info['audio_codec']}" src="${http_root}images/media_flags/audio_codec/${media_info['audio_codec'] | af}.png" /> <img class="summary-content-media-flag" title="${media_info['audio_codec']}" src="${http_root}images/media_flags/audio_codec/${media_info['audio_codec'] | af}.png" />
@@ -220,16 +269,33 @@ DOCUMENTATION :: END
<div class="summary-content"> <div class="summary-content">
<div class="summary-content-details-wrapper"> <div class="summary-content-details-wrapper">
% if data['rating']: % if data['rating']:
<div class="star-rating hidden-xs hidden-sm" title="${data['rating']}"> % if data['rating_image']:
% for i in range(0,5): % if data['rating_image'].startswith('imdb://'):
% if round(float(data['rating']) / 2) > i: <div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<i class="star-icon fa fa-star"></i> <span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
% else:
<i class="star-icon-o fa fa-star-o"></i>
% endif
% endfor
</div> </div>
% endif % endif
% if data['rating_image'].startswith('themoviedb://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<span class="rating-image rating-themoviedb"><strong>${get_percent(data['rating'], 10)}%</strong></span>
</div>
% endif
% if data['audience_rating_image'].startswith('rottentomatoes://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['audience_rating']}">
<span class="rating-image rating-rottentomatos-${data['audience_rating_image'].rsplit('.')[-1]}"><strong>${get_percent(data['audience_rating'], 10)}%</strong></span>
</div>
% endif
% if data['rating_image'].startswith('rottentomatoes://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<span class="rating-image rating-rottentomatos-${data['rating_image'].rsplit('.')[-1]}"><strong>${get_percent(data['rating'], 10)}%</strong></span>
</div>
% endif
% else:
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<i class="star-icon fa fa-star"></i> <strong>${get_percent(data['rating'], 10)}%</strong>
</div>
% endif
% endif
<div class="summary-content-details-tag"> <div class="summary-content-details-tag">
% if data['directors']: % if data['directors']:
Directed by <strong> ${data['directors'][0]}</strong> Directed by <strong> ${data['directors'][0]}</strong>
@@ -251,6 +317,8 @@ DOCUMENTATION :: END
Released <strong> ${data['year']}</strong> Released <strong> ${data['year']}</strong>
% elif data['media_type'] == 'collection': % elif data['media_type'] == 'collection':
Year <strong> ${data['min_year']} - ${data['max_year']}</strong> Year <strong> ${data['min_year']} - ${data['max_year']}</strong>
% elif data['year']:
Year <strong> ${data['year']}</strong>
% endif % endif
</div> </div>
<div class="summary-content-details-tag"> <div class="summary-content-details-tag">
@@ -263,6 +331,11 @@ DOCUMENTATION :: END
Rated <strong> ${data['content_rating']} </strong> Rated <strong> ${data['content_rating']} </strong>
% endif % endif
</div> </div>
<div class="summary-content-details-tag" id="channel-icon">
% if media_info['channel_identifier']:
Channel <strong> <span class="thumb-tooltip" data-toggle="popover" data-img="${media_info['channel_thumb']}" data-height="40" data-width="40">${media_info['channel_call_sign']} ${media_info['channel_identifier']}</span> </strong>
% endif
</div>
</div> </div>
% if data['tagline']: % if data['tagline']:
<div class="summary-content-summary"> <div class="summary-content-summary">
@@ -405,17 +478,17 @@ DOCUMENTATION :: END
</a> </a>
</div> </div>
% endif % endif
% if data.get('tvmaze_id') or data.get('themoviedb_id'): % if data.get('tvmaze_id') or data.get('themoviedb_id') or data.get('musicbrainz_id'):
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-danger btn-edit" data-toggle="modal" aria-pressed="false" autocomplete="off" id="delete-lookup-info" <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-id="${data['grandparent_rating_key'] if data['media_type'] == 'episode' else data['parent_rating_key'] if data['media_type'] == 'season' 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']}"> data-title="${data['grandparent_title'] if data['media_type'] == 'episode' else data['parent_title'] if data['media_type'] == 'season' else data['title']}">
<i class="fa fa-search"></i> Delete Lookup Info <i class="fa fa-search"></i> Delete Lookup Info
</button> </button>
</div> </div>
% endif % endif
% if data.get('poster_url'): % if data.get('poster_url'):
<div class="btn-group"> <div class="btn-group" id="hosted-poster">
% if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track': % if data['media_type'] == 'artist' or data['media_type'] == 'album' or data['media_type'] == 'track':
<span class="hosted-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: % else:
@@ -429,6 +502,7 @@ DOCUMENTATION :: END
</span> </span>
</div> </div>
% endif % endif
% if not data['live']:
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-dark" data-toggle="modal" aria-pressed="false" autocomplete="off" id="send-recently-added-notification" <button class="btn btn-dark" data-toggle="modal" aria-pressed="false" autocomplete="off" id="send-recently-added-notification"
data-id="${data['rating_key']}"> data-id="${data['rating_key']}">
@@ -436,6 +510,7 @@ DOCUMENTATION :: END
</button> </button>
</div> </div>
% endif % endif
% endif
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button> <button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
</div> </div>
@@ -451,6 +526,7 @@ DOCUMENTATION :: END
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="player">Player</th> <th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -473,6 +549,10 @@ DOCUMENTATION :: END
</%def> </%def>
<%def name="modalIncludes()"> <%def name="modalIncludes()">
% if metadata:
<%
data = defaultdict(None, **metadata)
%>
<div class="modal fade" id="info-modal" 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>
<div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal"> <div class="modal fade" id="ip-info-modal" tabindex="-1" role="dialog" aria-labelledby="ip-info-modal">
@@ -548,6 +628,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% endif
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
@@ -557,9 +638,28 @@ DOCUMENTATION :: END
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script> <script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
<script src="${http_root}js/moment-with-locale.js"></script> <script src="${http_root}js/moment-with-locale.js"></script>
% if data: % if metadata:
<%
data = defaultdict(None, **metadata)
%>
<script src="${http_root}js/tables/history_table.js${cache_param}"></script> <script src="${http_root}js/tables/history_table.js${cache_param}"></script>
% if data['media_type'] in ('show', 'artist'): % if data['live']:
<script>
function get_history() {
history_table_options.ajax = {
url: 'get_history',
type: 'POST',
data: function ( d ) {
return {
json_data: JSON.stringify( d ),
guid: "${data['guid']}",
user_id: "${_session['user_group']}" == "admin" ? null : "${_session['user_id']}"
};
}
}
}
</script>
% elif data['media_type'] in ('show', 'artist'):
<script> <script>
function get_history() { function get_history() {
history_table_options.ajax = { history_table_options.ajax = {
@@ -613,7 +713,7 @@ DOCUMENTATION :: END
$(document).ready(function () { $(document).ready(function () {
get_history(); get_history();
history_table = $('#history_table-RK-${data["rating_key"]}').DataTable(history_table_options); history_table = $('#history_table-RK-${data["rating_key"]}').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, 12] });
$(colvis.button()).appendTo('div.colvis-button-bar'); $(colvis.button()).appendTo('div.colvis-button-bar');
clearSearchButton('history_table-RK-${data["rating_key"]}', history_table); clearSearchButton('history_table-RK-${data["rating_key"]}', history_table);
@@ -626,19 +726,17 @@ DOCUMENTATION :: END
$('#deleteCount').text(history_to_delete.length); $('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function (row, idx) { $.ajax({
$.ajax({ url: 'delete_history_rows',
url: 'delete_history_rows', type: 'POST',
type: 'POST', data: { row_ids: history_to_delete.join(',') },
data: { row_id: row }, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "History deleted";
var msg = "History deleted"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); history_table.draw();
} }
});
}); });
history_table.draw();
}); });
} }
@@ -723,10 +821,22 @@ DOCUMENTATION :: END
$("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY')); $("#airdate").html(moment($("#airdate").text()).format('MMM DD, YYYY'));
$("#runtime").html(millisecondsToMinutes($("#runtime").text(), true)); $("#runtime").html(millisecondsToMinutes($("#runtime").text(), true));
$('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 }); $('div.art-face').animate({ opacity: 0.2 }, { duration: 1000 });
$('#channel-icon').popover({
selector: '[data-toggle=popover]',
html: true,
container: 'body',
trigger: 'hover',
placement: 'right',
template: '<div class="popover channel-thumbnail-popover" role="tooltip"><div class="arrow" style="top: 50%;"></div><div class="popover-content"></div></div>',
content: function () {
return '<div class="channel-thumbnail" style="background-image: url(' + $(this).data('img') + ');" />';
}
});
</script> </script>
% if data.get('poster_url'): % if data.get('poster_url'):
<script> <script>
$('.hosted-poster-tooltip').popover({ $('#hosted-poster').popover({
selector: '[data-toggle=popover]',
html: true, html: true,
container: 'body', container: 'body',
trigger: 'hover', trigger: 'hover',
@@ -750,13 +860,13 @@ DOCUMENTATION :: END
}); });
</script> </script>
% endif % endif
% if data.get('tvmaze_id') or data.get('themoviedb_id'): % if data.get('tvmaze_id') or data.get('themoviedb_id') or data.get('musicbrainz_id'):
<script> <script>
$('#delete-lookup-info').on('click', function () { $('#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>' + var msg = 'Are you sure you want to delete all the metadata lookup info for <strong>' + $(this).data('title') + '</strong>?' +
'The info will be looked up again the next time a notification is sent.'; '<br /><br />Tautulli will lookup the metadata info again the next time a notification is sent.';
var url = 'delete_lookup_info'; var url = 'delete_lookup_info';
var data = { rating_key: $(this).data('id'), title: $(this).data('title') }; var data = { rating_key: $(this).data('id') };
var callback = function () { var callback = function () {
$('#delete-lookup-info').closest('.btn-group').remove(); $('#delete-lookup-info').closest('.btn-group').remove();
}; };

View File

@@ -27,6 +27,9 @@ DOCUMENTATION :: END
</%doc> </%doc>
% if data != None: % if data != None:
<%
from plexpy.helpers import page
%>
% if data['children_count'] > 0: % if data['children_count'] > 0:
<div class="item-children-wrapper"> <div class="item-children-wrapper">
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
@@ -38,9 +41,9 @@ DOCUMENTATION :: END
<li> <li>
% endif % endif
% if data['children_type'] == 'movie': % if data['children_type'] == 'movie':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}"> <a href="${page('info', child['rating_key'])}" title="${child['title']}">
<div class="item-children-poster"> <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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -48,14 +51,14 @@ DOCUMENTATION :: END
</a> </a>
<div class="item-children-instance-text-wrapper poster-item"> <div class="item-children-instance-text-wrapper poster-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
<h3 class="text-muted">${child['year']}</h3> <h3 class="text-muted">${child['year']}</h3>
</div> </div>
% elif data['children_type'] == 'show': % elif data['children_type'] == 'show':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}"> <a href="${page('info', child['rating_key'])}" title="${child['title']}">
<div class="item-children-poster"> <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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -63,16 +66,16 @@ DOCUMENTATION :: END
</a> </a>
<div class="item-children-instance-text-wrapper poster-item"> <div class="item-children-instance-text-wrapper poster-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
</div> </div>
% elif data['children_type'] == 'season': % elif data['children_type'] == 'season':
<a href="info?rating_key=${child['rating_key']}" title="Season ${child['media_index']}"> <a href="${page('info', child['rating_key'])}" title="Season ${child['media_index']}">
<div class="item-children-poster"> <div class="item-children-poster">
% if child['thumb']: % if child['thumb']:
<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 class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});">
% else: % else:
<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);"> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 450, fallback='poster')});">
% endif % endif
<div class="item-children-card-overlay"> <div class="item-children-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
@@ -86,9 +89,9 @@ DOCUMENTATION :: END
</div> </div>
</a> </a>
% elif data['children_type'] == 'episode': % elif data['children_type'] == 'episode':
<a href="info?rating_key=${child['rating_key']}" title="Episode ${child['media_index']}"> <a href="${page('info', child['rating_key'])}" title="Episode ${child['media_index']}">
<div class="item-children-poster"> <div class="item-children-poster">
<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-poster-face episode-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 500, 280, fallback='art')});">
<div class="item-children-card-overlay"> <div class="item-children-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
Episode ${child['media_index'] or child['originally_available_at']} Episode ${child['media_index'] or child['originally_available_at']}
@@ -102,13 +105,13 @@ DOCUMENTATION :: END
</a> </a>
<div class="item-children-instance-text-wrapper episode-item"> <div class="item-children-instance-text-wrapper episode-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
</div> </div>
% elif data['children_type'] == 'album': % elif data['children_type'] == 'album':
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}"> <a href="${page('info', child['rating_key'])}" title="${child['title']}">
<div class="item-children-poster"> <div class="item-children-poster">
<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> <div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -116,14 +119,14 @@ DOCUMENTATION :: END
</a> </a>
<div class="item-children-instance-text-wrapper cover-item"> <div class="item-children-instance-text-wrapper cover-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
</div> </div>
% elif data['children_type'] == 'track': % elif data['children_type'] == 'track':
% if loop.index % 2 == 0: % if loop.index % 2 == 0:
<div class="item-children-list-item-even"> <div class="item-children-list-item-even">
<span class="item-children-list-item-index">&nbsp;${child['media_index']}</span> <span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
% if child['original_title']: % if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span> <span class="text-muted"> - ${child['original_title']}</span>
% endif % endif
@@ -135,7 +138,7 @@ DOCUMENTATION :: END
% else: % else:
<div class="item-children-list-item-odd"> <div class="item-children-list-item-odd">
<span class="item-children-list-item-index">&nbsp;${child['media_index']}</span> <span class="item-children-list-item-index">&nbsp;${child['media_index']}</span>
<span class="item-children-list-item-title"><a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <span class="item-children-list-item-title"><a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
% if child['original_title']: % if child['original_title']:
<span class="text-muted"> - ${child['original_title']}</span> <span class="text-muted"> - ${child['original_title']}</span>
% endif % endif

View File

@@ -29,6 +29,7 @@ DOCUMENTATION :: END
% if data != None: % if data != None:
<% <%
from plexpy.common import MEDIA_TYPE_HEADERS from plexpy.common import MEDIA_TYPE_HEADERS
from plexpy.helpers import page
types = ('movie', 'show', 'artist', 'album') types = ('movie', 'show', 'artist', 'album')
%> %>
% for media_type in types: % for media_type in types:
@@ -45,12 +46,12 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list'][media_type]: % for child in data['results_list'][media_type]:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}"> <a href="${page('info', child['rating_key'])}" title="${child['title']}">
<div class="item-children-poster"> <div class="item-children-poster">
% if media_type in ('artist', 'album'): % 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> <div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
% else: % 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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% endif % endif
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
@@ -60,22 +61,22 @@ DOCUMENTATION :: END
% if media_type == 'artist': % if media_type == 'artist':
<div class="item-children-instance-text-wrapper cover-item"> <div class="item-children-instance-text-wrapper cover-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
</div> </div>
% elif media_type == 'album': % elif media_type == 'album':
<div class="item-children-instance-text-wrapper cover-item"> <div class="item-children-instance-text-wrapper cover-item">
<h3> <h3>
<a href="info?rating_key=${child['parent_rating_key']}" title="${child['parent_title']}">${child['parent_title']}</a> <a href="${page('info', child['parent_rating_key'])}" title="${child['parent_title']}">${child['parent_title']}</a>
</h3> </h3>
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
</div> </div>
% else: % else:
<div class="item-children-instance-text-wrapper poster-item"> <div class="item-children-instance-text-wrapper poster-item">
<h3> <h3>
<a href="info?rating_key=${child['rating_key']}" title="${child['title']}">${child['title']}</a> <a href="${page('info', child['rating_key'])}" title="${child['title']}">${child['title']}</a>
</h3> </h3>
<h3 class="text-muted">${child['year']}</h3> <h3 class="text-muted">${child['year']}</h3>
</div> </div>

View File

@@ -53,6 +53,9 @@ DOCUMENTATION :: END
</%doc> </%doc>
% if data != None: % if data != None:
<%
from plexpy.helpers import page
%>
% if data['results_count'] > 0: % if data['results_count'] > 0:
% if 'collection' in data['results_list'] and data['results_list']['collection']: % if 'collection' in data['results_list'] and data['results_list']['collection']:
<div class="item-children-wrapper"> <div class="item-children-wrapper">
@@ -62,9 +65,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['collection']: % for child in data['results_list']['collection']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -87,9 +90,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['movie']: % for child in data['results_list']['movie']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -112,9 +115,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['show']: % for child in data['results_list']['show']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -137,9 +140,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['season']: % for child in data['results_list']['season']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <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> <div class="item-children-poster-face poster-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 450, fallback='poster')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -162,9 +165,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['episode']: % for child in data['results_list']['episode']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <div class="item-children-poster">
<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> <div class="item-children-poster-face episode-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 500, 280, fallback='art')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -188,9 +191,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['artist']: % for child in data['results_list']['artist']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <div class="item-children-poster">
<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> <div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -212,9 +215,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['album']: % for child in data['results_list']['album']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <div class="item-children-poster">
<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> <div class="item-children-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['thumb'], child['rating_key'], 300, 300, fallback='cover')});"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image" title="Refresh image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -237,9 +240,9 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled"> <ul class="item-children-instance list-unstyled">
% for child in data['results_list']['track']: % for child in data['results_list']['track']:
<li> <li>
<a href="info?rating_key=${child['rating_key']}" id="${child['rating_key']}"> <a href="${page('info', child['rating_key'])}" data-rating_key="${child['rating_key']}" data-library_name="${child['library_name']}">
<div class="item-children-poster"> <div class="item-children-poster">
<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-poster-face cover-item" style="background-image: url(${page('pms_image_proxy', child['parent_thumb'], child['parent_rating_key'], 300, 300, fallback='cover')});">
<div class="item-children-card-overlay"> <div class="item-children-card-overlay">
<div class="item-children-overlay-text"> <div class="item-children-overlay-text">
Track ${child['media_index']} Track ${child['media_index']}

View File

@@ -24,7 +24,6 @@
<div id="ip_error" class="col-sm-12 text-muted"></div> <div id="ip_error" class="col-sm-12 text-muted"></div>
<div class="col-sm-6"> <div class="col-sm-6">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li>Continent: <strong><span id="continent"></span></strong></li>
<li>Country: <strong><span id="country"></span></strong></li> <li>Country: <strong><span id="country"></span></strong></li>
<li>Region: <strong><span id="region"></span></strong></li> <li>Region: <strong><span id="region"></span></strong></li>
<li>City: <strong><span id="city"></span></strong></li> <li>City: <strong><span id="city"></span></strong></li>
@@ -36,7 +35,6 @@
<li>Timezone: <strong><span id="timezone"></span></strong></li> <li>Timezone: <strong><span id="timezone"></span></strong></li>
<li>Latitude: <strong><span id="latitude"></span></strong></li> <li>Latitude: <strong><span id="latitude"></span></strong></li>
<li>Longitude: <strong><span id="longitude"></span></strong></li> <li>Longitude: <strong><span id="longitude"></span></strong></li>
<li>Accuracy Radius: <strong><span id="accuracy"></span></strong></li>
</ul> </ul>
</div> </div>
<div class="col-sm-12"> <div class="col-sm-12">
@@ -61,8 +59,6 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<% from plexpy.helpers import anon_url %>
<span class="text-muted">GeoLite2 data created by <a href="${anon_url('http://www.maxmind.com')}" target="_blank">MaxMind</a>.</span>
</div> </div>
</div> </div>
</div> </div>
@@ -82,11 +78,11 @@
error: function () { error: function () {
$('#ip_error').html('<i class="fa fa-exclamation-circle"></i> Internal request failed.').show(); $('#ip_error').html('<i class="fa fa-exclamation-circle"></i> Internal request failed.').show();
}, },
success: function (data) { success: function (result) {
if ('error' in data) { if (result.result === 'error') {
$('#ip_error').html('<i class="fa fa-exclamation-circle"></i> ' + data.error).show(); $('#ip_error').html('<i class="fa fa-exclamation-circle"></i> ' + result.message).show();
} else { } else {
$('#continent').html(data.continent); var data = result.data;
$('#country').html(data.country); $('#country').html(data.country);
$('#region').html(data.region); $('#region').html(data.region);
$('#city').html(data.city); $('#city').html(data.city);
@@ -94,7 +90,6 @@
$('#timezone').html(data.timezone); $('#timezone').html(data.timezone);
$('#latitude').html(data.latitude); $('#latitude').html(data.latitude);
$('#longitude').html(data.longitude); $('#longitude').html(data.longitude);
$('#accuracy').html(data.accuracy + ' km');
} }
} }
}); });

View File

@@ -36,7 +36,3 @@ function check_notifications() {
check_notifications(); check_notifications();
}, 5000); }, 5000);
} }
$(document).ready(function () {
check_notifications();
});

View File

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

View File

@@ -23,7 +23,6 @@ var hc_plays_by_dayofweek_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_hourofday_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_month_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
labels: { labels: {
style: { style: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_platform_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_platform_by_stream_type_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_source_resolution_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_stream_resolution_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

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

View File

@@ -23,7 +23,6 @@ var hc_plays_by_user_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -23,7 +23,6 @@ var hc_plays_by_user_by_stream_type_options = {
credits: { credits: {
enabled: false enabled: false
}, },
colors: ['#F9AA03', '#FFFFFF', '#FF4747'],
xAxis: { xAxis: {
categories: [{}], categories: [{}],
labels: { labels: {

View File

@@ -10,17 +10,33 @@ if (typeof platform !== 'undefined') {
} }
if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) { if (['IE', 'Microsoft Edge', 'IE Mobile'].indexOf(p.name) > -1) {
$('body').prepend('<div id="browser-warning"><i class="fa fa-exclamation-circle"></i>&nbsp;' + if (!getCookie('browserDismiss')) {
'Tautulli does not support Internet Explorer or Microsoft Edge! ' + var $browser_warning = $('<div id="browser-warning">' +
'Please use a different browser such as Chrome or Firefox.</div>'); '<i class="fa fa-exclamation-circle"></i>&nbsp;' +
var offset = $('#browser-warning').height(); 'Tautulli does not support Internet Explorer or Microsoft Edge! ' +
var navbar = $('.navbar-fixed-top'); 'Please use a different browser such as Chrome or Firefox.' +
if (navbar.length) { '<button type="button" class="close"><i class="fa fa-remove"></i></button>' +
navbar.offset({top: navbar.offset().top + offset}); '</div>');
} $('body').prepend($browser_warning);
var container = $('.body-container'); var offset = $browser_warning.height();
if (container.length) { warningOffset(offset);
container.offset({top: container.offset().top + offset});
$browser_warning.one('click', 'button.close', function () {
$browser_warning.remove();
warningOffset(-offset);
setCookie('browserDismiss', 'true', 7);
});
function warningOffset(offset) {
var navbar = $('.navbar-fixed-top');
if (navbar.length) {
navbar.offset({top: navbar.offset().top + offset});
}
var container = $('.body-container');
if (container.length) {
container.offset({top: container.offset().top + offset});
}
}
} }
} }
@@ -221,6 +237,27 @@ function doAjaxCall(url, elem, reload, form, showMsg, callback) {
}); });
} }
getBrowsePath = function (key, path, filter_ext) {
var deferred = $.Deferred();
$.ajax({
url: 'browse_path',
type: 'GET',
data: {
key: key,
path: path,
filter_ext: filter_ext
},
success: function(data) {
deferred.resolve(data);
},
error: function() {
deferred.reject();
}
});
return deferred;
};
function doSimpleAjaxCall(url) { function doSimpleAjaxCall(url) {
$.ajax(url); $.ajax(url);
} }
@@ -242,33 +279,31 @@ $.cachedScript = function (url) {
function isPrivateIP(ip_address) { function isPrivateIP(ip_address) {
var defer = $.Deferred(); var defer = $.Deferred();
$.cachedScript('js/ipaddr.min.js').done(function () { if (ipaddr.isValid(ip_address)) {
if (ipaddr.isValid(ip_address)) { var addr = ipaddr.process(ip_address);
var addr = ipaddr.process(ip_address);
var rangeList = []; var rangeList = [];
if (addr.kind() === 'ipv4') { if (addr.kind() === 'ipv4') {
rangeList = [ rangeList = [
ipaddr.parseCIDR('127.0.0.0/8'), ipaddr.parseCIDR('127.0.0.0/8'),
ipaddr.parseCIDR('10.0.0.0/8'), ipaddr.parseCIDR('10.0.0.0/8'),
ipaddr.parseCIDR('172.16.0.0/12'), ipaddr.parseCIDR('172.16.0.0/12'),
ipaddr.parseCIDR('192.168.0.0/16') ipaddr.parseCIDR('192.168.0.0/16')
]; ];
} else {
rangeList = [
ipaddr.parseCIDR('fd00::/8')
];
}
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
defer.resolve();
} else {
defer.reject();
}
} else { } else {
defer.resolve('n/a'); rangeList = [
ipaddr.parseCIDR('fd00::/8')
];
} }
});
if (ipaddr.subnetMatch(addr, rangeList, -1) >= 0) {
defer.resolve();
} else {
defer.reject();
}
} else {
defer.resolve('n/a');
}
return defer.promise(); return defer.promise();
} }
@@ -447,8 +482,9 @@ $('*').on('click', '.refresh_pms_image', function (e) {
}); });
// Taken from http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable#answer-14919494 // Taken from http://stackoverflow.com/questions/10420352/converting-file-size-in-bytes-to-human-readable#answer-14919494
function humanFileSize(bytes, si) { function humanFileSize(bytes, si = true) {
var thresh = si ? 1000 : 1024; //var thresh = si ? 1000 : 1024;
var thresh = 1024; // Always divide by 2^10 but display SI units
if (Math.abs(bytes) < thresh) { if (Math.abs(bytes) < thresh) {
return bytes + ' B'; return bytes + ' B';
} }
@@ -517,23 +553,26 @@ function PopupCenter(url, title, w, h) {
return newWindow; return newWindow;
} }
function setLocalStorage(key, value) {
function setLocalStorage(key, value, path) {
if (path !== false) {
key = key + '_' + window.location.pathname;
}
localStorage.setItem(key, value); localStorage.setItem(key, value);
} }
function getLocalStorage(key, default_value) { function getLocalStorage(key, default_value, path) {
if (path !== false) {
key = key + '_' + window.location.pathname;
}
var value = localStorage.getItem(key); var value = localStorage.getItem(key);
if (value !== null) { if (value !== null) {
return value return value
} else if (default_value !== undefined) { } else if (default_value !== undefined) {
setLocalStorage(key, default_value); setLocalStorage(key, default_value, path);
return default_value return default_value
} }
} }
if (!getLocalStorage('Tautulli_ClientId')) {
setLocalStorage('Tautulli_ClientId', uuidv4());
}
function uuidv4() { function uuidv4() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) { return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, function(c) {
var cryptoObj = window.crypto || window.msCrypto; // for IE 11 var cryptoObj = window.crypto || window.msCrypto; // for IE 11
@@ -541,16 +580,21 @@ function uuidv4() {
}); });
} }
var x_plex_headers = { function getPlexHeaders() {
'Accept': 'application/json', return {
'X-Plex-Product': 'Tautulli', 'Accept': 'application/json',
'X-Plex-Version': 'Plex OAuth', 'X-Plex-Product': 'Tautulli',
'X-Plex-Client-Identifier': localStorage.getItem('Tautulli_ClientId'), 'X-Plex-Version': 'Plex OAuth',
'X-Plex-Platform': p.name, 'X-Plex-Client-Identifier': getLocalStorage('Tautulli_ClientID', uuidv4(), false),
'X-Plex-Platform-Version': p.version, 'X-Plex-Platform': p.name,
'X-Plex-Device': p.os, 'X-Plex-Platform-Version': p.version,
'X-Plex-Device-Name': p.name 'X-Plex-Model': 'Plex OAuth',
}; 'X-Plex-Device': p.os,
'X-Plex-Device-Name': p.name,
'X-Plex-Device-Screen-Resolution': window.screen.width + 'x' + window.screen.height,
'X-Plex-Language': 'en'
};
}
var plex_oauth_window = null; var plex_oauth_window = null;
const plex_oauth_loader = '<style>' + const plex_oauth_loader = '<style>' +
@@ -601,6 +645,7 @@ function closePlexOAuthWindow() {
} }
getPlexOAuthPin = function () { getPlexOAuthPin = function () {
var x_plex_headers = getPlexHeaders();
var deferred = $.Deferred(); var deferred = $.Deferred();
$.ajax({ $.ajax({
@@ -629,10 +674,25 @@ function PlexOAuth(success, error, pre) {
$(plex_oauth_window.document.body).html(plex_oauth_loader); $(plex_oauth_window.document.body).html(plex_oauth_loader);
getPlexOAuthPin().then(function (data) { getPlexOAuthPin().then(function (data) {
var x_plex_headers = getPlexHeaders();
const pin = data.pin; const pin = data.pin;
const code = data.code; const code = data.code;
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?clientID=' + x_plex_headers['X-Plex-Client-Identifier'] + '&code=' + code; var oauth_params = {
'clientID': x_plex_headers['X-Plex-Client-Identifier'],
'context[device][product]': x_plex_headers['X-Plex-Product'],
'context[device][version]': x_plex_headers['X-Plex-Version'],
'context[device][platform]': x_plex_headers['X-Plex-Platform'],
'context[device][platformVersion]': x_plex_headers['X-Plex-Platform-Version'],
'context[device][device]': x_plex_headers['X-Plex-Device'],
'context[device][deviceName]': x_plex_headers['X-Plex-Device-Name'],
'context[device][model]': x_plex_headers['X-Plex-Model'],
'context[device][screenResolution]': x_plex_headers['X-Plex-Device-Screen-Resolution'],
'context[device][layout]': 'desktop',
'code': code
}
plex_oauth_window.location = 'https://app.plex.tv/auth/#!?' + encodeData(oauth_params);
polling = pin; polling = pin;
(function poll() { (function poll() {
@@ -670,4 +730,76 @@ function PlexOAuth(success, error, pre) {
error() error()
} }
}); });
} }
function encodeData(data) {
return Object.keys(data).map(function(key) {
return [key, data[key]].map(encodeURIComponent).join("=");
}).join("&");
}
function page(endpoint, ...args) {
let endpoints = {
'pms_image_proxy': pms_image_proxy,
'info': info_page,
'library': library_page,
'user': user_page
};
var params = {};
if (endpoint in endpoints) {
params = endpoints[endpoint](...args);
}
return endpoint + '?' + $.param(params).replace(/'/g, '%27');
}
function pms_image_proxy(img, rating_key, width, height, opacity, background, blur, fallback, refresh, clip, img_format) {
var params = {};
if (img != null) { params.img = img; }
if (rating_key != null) { params.rating_key = rating_key; }
if (width != null) { params.width = width; }
if (height != null) { params.height = height; }
if (opacity != null) { params.opacity = opacity; }
if (background != null) { params.background = background; }
if (blur != null) { params.blur = blur; }
if (fallback != null) { params.fallback = fallback; }
if (refresh != null) { params.refresh = true; }
if (clip != null) { params.clip = true; }
if (img_format != null) { params.img_format = img_format; }
return params;
}
function info_page(rating_key, guid, history, live) {
var params = {};
if (live && history) {
params.guid = guid;
} else {
params.rating_key = rating_key;
}
if (history) { params.source = 'history'; }
return params;
}
function library_page(section_id) {
var params = {};
if (section_id != null) { params.section_id = section_id; }
return params;
}
function user_page(user_id, user) {
var params = {};
if (user_id != null) { params.user_id = user_id; }
if (user != null) { params.user = user; }
return params;
}

View File

@@ -36,10 +36,10 @@ history_table_options = {
"targets": [0], "targets": [0],
"data": null, "data": null,
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (rowData['id'] === null) { if (rowData['row_id'] === null) {
$(td).html(''); $(td).html('');
} else { } else {
$(td).html('<button class="btn btn-xs btn-warning" data-id="' + rowData['id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>'); $(td).html('<button class="btn btn-xs btn-warning" data-id="' + rowData['row_id'] + '"><i class="fa fa-trash-o fa-fw"></i> Delete</button>');
} }
}, },
"width": "5%", "width": "5%",
@@ -49,7 +49,7 @@ history_table_options = {
}, },
{ {
"targets": [1], "targets": [1],
"data":"date", "data": "date",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
var date = moment(cellData, "X").format(date_format); var date = moment(cellData, "X").format(date_format);
if (rowData['state'] !== null) { if (rowData['state'] !== null) {
@@ -60,6 +60,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>'; 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') { } 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>'; 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'] === 'error') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Playback Error"><i class="fa fa-exclamation-triangle fa-fw"></i></span>';
} else if (rowData['state'] === 'stopped') { } 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>'; state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
} }
@@ -77,13 +79,13 @@ history_table_options = {
}, },
{ {
"targets": [2], "targets": [2],
"data":"friendly_name", "data": "friendly_name",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
if (rowData['user_id']) { if (rowData['user_id']) {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
} else { } else {
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
} }
} else { } else {
$(td).html(cellData); $(td).html(cellData);
@@ -112,7 +114,7 @@ history_table_options = {
}, },
{ {
"targets": [4], "targets": [4],
"data":"platform", "data": "platform",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
$(td).html(cellData); $(td).html(cellData);
@@ -123,6 +125,17 @@ history_table_options = {
}, },
{ {
"targets": [5], "targets": [5],
"data": "product",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "10%",
"className": "no-wrap"
},
{
"targets": [6],
"data": "player", "data": "player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@@ -130,53 +143,63 @@ history_table_options = {
if (rowData['transcode_decision'] === 'transcode') { if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') { } else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') { } else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
} }
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>'); $(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>');
} }
}, },
"width": "12%", "width": "10%",
"className": "no-wrap modal-control" "className": "no-wrap modal-control"
}, },
{ {
"targets": [6], "targets": [7],
"data":"full_title", "data": "full_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
var icon = '';
var icon_title = '';
var parent_info = ''; var parent_info = '';
var media_type = ''; var media_type = '';
var thumb_popover = ''; var thumb_popover = '';
var source = (rowData['state'] === null) ? 'source=history&' : ''; var fallback = (rowData['live']) ? 'poster-live' : 'poster';
var history = (rowData['state'] === null);
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } 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>'; media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' 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="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') { } else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')'; } icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>'; icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
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>' if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') { } else if (rowData['media_type'] === 'track') {
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; } if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>'; 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">' + cellData + parent_info + '</span>' thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?' + source + 'rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], history, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'clip') { } else if (rowData['media_type'] === 'clip') {
$(td).html(cellData); media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Clip"><i class="fa fa-video-camera fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else { } else {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
} }
} }
}, },
"width": "33%", "width": "25%",
"className": "datatable-wrap" "className": "datatable-wrap"
}, },
{ {
"targets": [7], "targets": [8],
"data":"started", "data": "started",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null) { if (cellData === null) {
$(td).html('n/a'); $(td).html('n/a');
@@ -189,8 +212,8 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [8], "targets": [9],
"data":"paused_counter", "data": "paused_counter",
"render": function (data, type, full) { "render": function (data, type, full) {
if (data !== null) { if (data !== null) {
return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins'; return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins';
@@ -203,8 +226,8 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [9], "targets": [10],
"data":"stopped", "data": "stopped",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData === null || (rowData['state'] != null && rowData['state'] != "stopped")) { if (cellData === null || (rowData['state'] != null && rowData['state'] != "stopped")) {
$(td).html('n/a'); $(td).html('n/a');
@@ -217,8 +240,8 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [10], "targets": [11],
"data":"duration", "data": "duration",
"render": function (data, type, full) { "render": function (data, type, full) {
if (data !== null) { if (data !== null) {
return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins'; return Math.round(moment.duration(data, 'seconds').as('minutes')) + ' mins';
@@ -231,7 +254,7 @@ history_table_options = {
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [11], "targets": [12],
"data": "watched_status", "data": "watched_status",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData == 1) { if (cellData == 1) {
@@ -298,19 +321,19 @@ history_table_options = {
"rowCallback": function (row, rowData, rowIndex) { "rowCallback": function (row, rowData, rowIndex) {
if (rowData['group_count'] == 1) { if (rowData['group_count'] == 1) {
// if no grouped rows simply toggle the delete button // if no grouped rows simply toggle the delete button
if ($.inArray(rowData['id'], history_to_delete) !== -1) { if ($.inArray(rowData['row_id'], history_to_delete) !== -1) {
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); $(row).find('button[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
} }
} else if (rowData['id'] !== null) { } else if (rowData['row_id'] !== null) {
// if grouped rows // if grouped rows
// toggle the parent button to danger // toggle the parent button to danger
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); $(row).find('button[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
// check if any child rows are not selected // check if any child rows are not selected
var group_ids = rowData['group_ids'].split(',').map(Number); var group_ids = rowData['group_ids'].split(',').map(Number);
group_ids.forEach(function (id) { group_ids.forEach(function (id) {
var index = $.inArray(id, history_to_delete); var index = $.inArray(id, history_to_delete);
if (index == -1) { if (index == -1) {
$(row).find('button[data-id="' + rowData['id'] + '"]').addClass('btn-warning').removeClass('btn-danger'); $(row).find('button[data-id="' + rowData['row_id'] + '"]').addClass('btn-warning').removeClass('btn-danger');
} }
}); });
} }
@@ -334,7 +357,7 @@ $('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
var rowData = row.data(); var rowData = row.data();
$.get('get_stream_data', { $.get('get_stream_data', {
row_id: rowData['id'], row_id: rowData['row_id'],
session_key: rowData['session_key'], session_key: rowData['session_key'],
user: rowData['friendly_name'] user: rowData['friendly_name']
}).then(function (jqXHR) { }).then(function (jqXHR) {
@@ -363,9 +386,9 @@ $('.history_table').on('click', '> tbody > tr > td.delete-control > button', fun
if (rowData['group_count'] == 1) { if (rowData['group_count'] == 1) {
// if no grouped rows simply add or remove row from history_to_delete // if no grouped rows simply add or remove row from history_to_delete
var index = $.inArray(rowData['id'], history_to_delete); var index = $.inArray(rowData['row_id'], history_to_delete);
if (index === -1) { if (index === -1) {
history_to_delete.push(rowData['id']); history_to_delete.push(rowData['row_id']);
} else { } else {
history_to_delete.splice(index, 1); history_to_delete.splice(index, 1);
} }
@@ -489,7 +512,8 @@ function childTableFormat(rowData) {
'<th align="left" id="friendly_name">User</th>' + '<th align="left" id="friendly_name">User</th>' +
'<th align="left" id="ip_address">IP Address</th>' + '<th align="left" id="ip_address">IP Address</th>' +
'<th align="left" id="platform">Platform</th>' + '<th align="left" id="platform">Platform</th>' +
'<th align="left" id="platform">Player</th>' + '<th align="left" id="product">Product</th>' +
'<th align="left" id="player">Player</th>' +
'<th align="left" id="title">Title</th>' + '<th align="left" id="title">Title</th>' +
'<th align="left" id="started">Started</th>' + '<th align="left" id="started">Started</th>' +
'<th align="left" id="paused_counter">Paused</th>' + '<th align="left" id="paused_counter">Paused</th>' +
@@ -529,7 +553,7 @@ function createChildTable(row, rowData) {
var childRowData = childRow.data(); var childRowData = childRow.data();
$.get('get_stream_data', { $.get('get_stream_data', {
row_id: childRowData['id'], row_id: childRowData['row_id'],
user: childRowData['friendly_name'] user: childRowData['friendly_name']
}).then(function (jqXHR) { }).then(function (jqXHR) {
$("#info-modal").html(jqXHR); $("#info-modal").html(jqXHR);
@@ -556,9 +580,9 @@ function createChildTable(row, rowData) {
var childRowData = childRow.data(); var childRowData = childRow.data();
// add or remove row from history_to_delete // add or remove row from history_to_delete
var index = $.inArray(childRowData['id'], history_to_delete); var index = $.inArray(childRowData['row_id'], history_to_delete);
if (index === -1) { if (index === -1) {
history_to_delete.push(childRowData['id']); history_to_delete.push(childRowData['row_id']);
} else { } else {
history_to_delete.splice(index, 1); history_to_delete.splice(index, 1);
} }

View File

@@ -63,9 +63,9 @@ history_table_modal_options = {
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
if (rowData['user_id']) { if (rowData['user_id']) {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
} else { } else {
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
} }
} else { } else {
$(td).html(cellData); $(td).html(cellData);
@@ -83,7 +83,7 @@ history_table_modal_options = {
if (rowData['transcode_decision'] === 'transcode') { if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') { } else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') { } else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
} }
@@ -98,26 +98,34 @@ history_table_modal_options = {
"data":"full_title", "data":"full_title",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
var icon = '';
var icon_title = '';
var parent_info = ''; var parent_info = '';
var media_type = ''; var media_type = '';
var thumb_popover = ''; var thumb_popover = '';
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } 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>'; media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' 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="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') { } else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')'; } icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>'; icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
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>' if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') { } else if (rowData['media_type'] === 'track') {
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; } if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>'; 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">' + cellData + parent_info + '</span>' thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else { } else {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
} }
} }
}, },
@@ -161,7 +169,7 @@ $('.history_table').on('click', 'td.modal-control', function () {
function showStreamDetails() { function showStreamDetails() {
$.ajax({ $.ajax({
url: 'get_stream_data', url: 'get_stream_data',
data: { row_id: rowData['id'], user: rowData['friendly_name'] }, data: { row_id: rowData['row_id'], user: rowData['friendly_name'] },
cache: false, cache: false,
async: true, async: true,
complete: function (xhr, status) { complete: function (xhr, status) {

View File

@@ -27,8 +27,8 @@ libraries_list_table_options = {
"data": null, "data": null,
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<div class="edit-library-toggles">' + $(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 delete-library" data-id="' + rowData['row_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' + '<button class="btn btn-xs btn-warning purge-library" data-id="' + rowData['row_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&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="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' +
'</div>'); '</div>');
}, },
@@ -41,14 +41,16 @@ libraries_list_table_options = {
"targets": [1], "targets": [1],
"data": "library_thumb", "data": "library_thumb",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
var inactive = '';
if (!rowData['is_active']) { inactive = '<span class="inactive-library-tooltip" data-toggle="tooltip" title="Library not on Plex server"><i class="fa fa-exclamation-triangle"></i></span>'; }
if (cellData !== null && cellData !== '') { if (cellData !== null && cellData !== '') {
if (rowData['library_thumb'].substring(0, 4) == "http") { if (rowData['library_thumb'].substring(0, 4) == "http") {
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(' + rowData['library_thumb'] + ');"></div></a>'); $(td).html('<a href="' + page('library', rowData['section_id']) + '"><div class="libraries-poster-face" style="background-image: url(' + rowData['library_thumb'] + ');">' + inactive + '</div></a>');
} else { } else {
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face svg-icon library-' + rowData['section_type'] + '"></div></a>'); $(td).html('<a href="' + page('library', rowData['section_id']) + '"><div class="libraries-poster-face svg-icon library-' + rowData['section_type'] + '">' + inactive + '</div></a>');
} }
} else { } else {
$(td).html('<a href="library?section_id=' + rowData['section_id'] + '"><div class="libraries-poster-face" style="background-image: url(../../images/cover.png);"></div></a>'); $(td).html('<a href="' + page('library', rowData['section_id']) + '"><div class="libraries-poster-face" style="background-image: url(../../images/cover.png);">' + inactive + '</div></a>');
} }
}, },
"orderable": false, "orderable": false,
@@ -61,8 +63,8 @@ libraries_list_table_options = {
"data": "section_name", "data": "section_name",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') { if (cellData !== null && cellData !== '') {
$(td).html('<div data-id="' + rowData['section_id'] + '">' + $(td).html('<div data-id="' + rowData['row_id'] + '">' +
'<a href="library?section_id=' + rowData['section_id'] + '">' + cellData + '</a>' + '<a href="' + page('library', rowData['section_id']) + '">' + cellData + '</a>' +
'</div>'); '</div>');
} else { } else {
$(td).html('n/a'); $(td).html('n/a');
@@ -137,45 +139,34 @@ libraries_list_table_options = {
"data":"last_played", "data":"last_played",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') { if (cellData !== null && cellData !== '') {
var icon = '';
var icon_title = '';
var parent_info = ''; var parent_info = '';
var media_type = ''; var media_type = '';
var thumb_popover = ''; var thumb_popover = '';
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Movie"><i class="fa fa-film fa-fw"></i></span>'; icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
if (rowData['rating_key']) { icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; }
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>' media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
} else { $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="images/poster.png" data-height="120" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
}
} else if (rowData['media_type'] === 'episode') { } 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>'; icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
if (rowData['rating_key']) { icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')'; } if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
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>' else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
} else { thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="images/poster.png" data-height="120" data-width="80">' + cellData + parent_info + '</span>' $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
$(td).html('<div class="history-title"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
}
} else if (rowData['media_type'] === 'track') { } else if (rowData['media_type'] === 'track') {
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>'; media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>';
if (rowData['rating_key']) { thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; } $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
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 + parent_info + '</span>'
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else {
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="images/cover.png" data-height="80" data-width="80">' + cellData + parent_info + '</span>'
$(td).html('<div class="history-title"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
}
} else if (rowData['media_type']) { } else if (rowData['media_type']) {
if (rowData['rating_key']) { $(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>');
} else {
$(td).html(cellData);
}
} }
} else { } else {
$(td).html('n/a'); $(td).html('n/a');
@@ -243,11 +234,11 @@ libraries_list_table_options = {
showMsg(msg, false, false, 0) showMsg(msg, false, false, 0)
}, },
"rowCallback": function (row, rowData) { "rowCallback": function (row, rowData) {
if ($.inArray(rowData['section_id'], libraries_to_delete) !== -1) { if ($.inArray(rowData['row_id'], libraries_to_delete) !== -1) {
$(row).find('button.delete-library[data-id="' + rowData['section_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); $(row).find('button.delete-library[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
} }
if ($.inArray(rowData['section_id'], libraries_to_purge) !== -1) { if ($.inArray(rowData['row_id'], libraries_to_purge) !== -1) {
$(row).find('button.purge-library[data-id="' + rowData['section_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); $(row).find('button.purge-library[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
} }
} }
} }
@@ -288,11 +279,11 @@ $('#libraries_list_table').on('click', 'td.edit-control > .edit-library-toggles
var row = libraries_list_table.row(tr); var row = libraries_list_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var index_delete = $.inArray(rowData['section_id'], libraries_to_delete); var index_delete = $.inArray(rowData['row_id'], libraries_to_delete);
var index_purge = $.inArray(rowData['section_id'], libraries_to_purge); var index_purge = $.inArray(rowData['row_id'], libraries_to_purge);
if (index_delete === -1) { if (index_delete === -1) {
libraries_to_delete.push(rowData['section_id']); libraries_to_delete.push(rowData['row_id']);
if (index_purge === -1) { if (index_purge === -1) {
tr.find('button.purge-library').click(); tr.find('button.purge-library').click();
} }
@@ -311,11 +302,11 @@ $('#libraries_list_table').on('click', 'td.edit-control > .edit-library-toggles
var row = libraries_list_table.row(tr); var row = libraries_list_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var index_delete = $.inArray(rowData['section_id'], libraries_to_delete); var index_delete = $.inArray(rowData['row_id'], libraries_to_delete);
var index_purge = $.inArray(rowData['section_id'], libraries_to_purge); var index_purge = $.inArray(rowData['row_id'], libraries_to_purge);
if (index_purge === -1) { if (index_purge === -1) {
libraries_to_purge.push(rowData['section_id']); libraries_to_purge.push(rowData['row_id']);
} else { } else {
libraries_to_purge.splice(index_purge, 1); libraries_to_purge.splice(index_purge, 1);
if (index_delete != -1) { if (index_delete != -1) {

View File

@@ -50,7 +50,7 @@ media_info_table_options = {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Episodes"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Episodes"><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>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'artist') { } else if (rowData['media_type'] === 'artist') {
expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Albumns"><i class="fa fa-plus-circle fa-fw"></i></span>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Albums"><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>'); $(td).html('<div><a href="#"><div style="float: left;">' + expand_details + '&nbsp;' + date + '</div></a></div>');
} else if (rowData['media_type'] === 'album') { } 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>'; expand_details = '<span class="expand-media-info-tooltip" data-toggle="tooltip" title="Show Tracks"><i class="fa fa-plus-circle fa-fw"></i></span>';
@@ -78,43 +78,43 @@ media_info_table_options = {
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } 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>'; 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">' + rowData['title'] + parent_info + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'show') { } 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>'; 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">' + rowData['title'] + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'season') { } 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>'; 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">' + rowData['title'] + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') { } 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>'; 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'] + ' - ' + rowData['title'] + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 500, 280, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 30px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'artist') { } 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>'; 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">' + rowData['title'] + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'album') { } 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>'; 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">' + rowData['title'] + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key']) + '"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') { } 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>'; 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'] + ' - ' + rowData['title'] + '</span>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, '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>'); $(td).html('<div class="history-title"><a href="' + page('info', 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') { } 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>'; 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>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, '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>'); $(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') { } 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>'; 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>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, '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>'); $(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') { } 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>'; 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>'; thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 500, 280, null, null, null, '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>'); $(td).html('<div class="history-title"><div style="float: left; padding-left: 15px;">' + media_type + '&nbsp;' + thumb_popover + '</div></div>');
} else { } else {
$(td).html(cellData); $(td).html(cellData);

View File

@@ -51,9 +51,9 @@ sync_table_options = {
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
if (rowData['user_id']) { if (rowData['user_id']) {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
} else { } else {
$(td).html('<a href="user?user=' + rowData['user'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
} }
} else { } else {
$(td).html(cellData); $(td).html(cellData);
@@ -67,7 +67,7 @@ sync_table_options = {
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
if (rowData['rating_key']) { if (rowData['rating_key']) {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
} else { } else {
$(td).html(cellData); $(td).html(cellData);
} }

View File

@@ -1,3 +1,25 @@
var date_format = 'YYYY-MM-DD';
var time_format = 'hh:mm a';
$.ajax({
url: 'get_date_formats',
type: 'GET',
success: function(data) {
date_format = data.date_format;
time_format = data.time_format;
}
});
var seenRender = function (data, type, full) {
return moment(data, "X").fromNow();
};
var seenCreatedCell = function (td, cellData, rowData, row, col) {
if (cellData !== null) {
$(td).attr('title', moment(cellData, "X").format(date_format + ' ' + time_format));
}
};
user_ip_table_options = { user_ip_table_options = {
"destroy": true, "destroy": true,
"language": { "language": {
@@ -21,16 +43,24 @@ user_ip_table_options = {
"columnDefs": [ "columnDefs": [
{ {
"targets": [0], "targets": [0],
"data":"last_seen", "data": "last_seen",
"render": function ( data, type, full ) { "render": seenRender,
return moment(data, "X").fromNow(); "createdCell": seenCreatedCell,
},
"searchable": false, "searchable": false,
"width": "15%", "width": "12%",
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [1], "targets": [1],
"data": "first_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "12%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "ip_address", "data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData) { if (cellData) {
@@ -44,22 +74,22 @@ user_ip_table_options = {
$(td).html('n/a'); $(td).html('n/a');
} }
}, },
"width": "15%", "width": "12%",
"className": "no-wrap modal-control-ip" "className": "no-wrap modal-control-ip"
}, },
{ {
"targets": [2], "targets": [3],
"data": "platform", "data": "platform",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
$(td).html(cellData); $(td).html(cellData);
} }
}, },
"width": "15%", "width": "12%",
"className": "no-wrap" "className": "no-wrap"
}, },
{ {
"targets": [3], "targets": [4],
"data": "player", "data": "player",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
@@ -67,51 +97,59 @@ user_ip_table_options = {
if (rowData['transcode_decision'] === 'transcode') { if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') { } else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') { } else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
} }
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>'); $(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>');
} }
}, },
"width": "15%", "width": "12%",
"className": "no-wrap modal-control" "className": "no-wrap modal-control"
}, },
{ {
"targets": [4], "targets": [5],
"data": "last_played", "data": "last_played",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') { if (cellData !== '') {
var icon = '';
var icon_title = '';
var parent_info = ''; var parent_info = '';
var media_type = ''; var media_type = '';
var thumb_popover = ''; var thumb_popover = '';
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } 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>'; media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' 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="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') { } else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')'; } icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>'; icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
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>' if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') { } else if (rowData['media_type'] === 'track') {
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; } if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>'; 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">' + cellData + parent_info + '</span>' thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type']) { } else if (rowData['media_type']) {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
} else {
$(td).html('n/a');
} }
} else {
$(td).html('n/a');
} }
}, },
"width": "30%", "width": "30%",
"className": "datatable-wrap" "className": "datatable-wrap"
}, },
{ {
"targets": [5], "targets": [6],
"data": "play_count", "data": "play_count",
"searchable": false, "searchable": false,
"width": "10%", "width": "10%",
@@ -159,7 +197,7 @@ $('.user_ip_table').on('click', 'td.modal-control', function () {
function showStreamDetails() { function showStreamDetails() {
$.ajax({ $.ajax({
url: 'get_stream_data', url: 'get_stream_data',
data: { row_id: rowData['id'], user: rowData['friendly_name'] }, data: { row_id: rowData['history_row_id'], user: rowData['friendly_name'] },
cache: false, cache: false,
async: true, async: true,
complete: function (xhr, status) { complete: function (xhr, status) {

View File

@@ -44,8 +44,8 @@ users_list_table_options = {
"data": null, "data": null,
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<div class="edit-user-toggles">' + $(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 delete-user" data-id="' + rowData['row_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' + '<button class="btn btn-xs btn-warning purge-user" data-id="' + rowData['row_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&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="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' + '<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>'); '</div>');
@@ -59,10 +59,12 @@ users_list_table_options = {
"targets": [1], "targets": [1],
"data": "user_thumb", "data": "user_thumb",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
var inactive = '';
if (!rowData['is_active']) { inactive = '<span class="inactive-user-tooltip" data-toggle="tooltip" title="User not on Plex server"><i class="fa fa-exclamation-triangle"></i></span>'; }
if (cellData === '') { if (cellData === '') {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);"></div></a>'); $(td).html('<a href="' + page('user', rowData['user_id']) + '"" title="' + rowData['username'] + '"><div class="users-poster-face" style="background-image: url(../../images/gravatar-default-80x80.png);">' + inactive + '</div></a>');
} else { } else {
$(td).html('<a href="user?user_id=' + rowData['user_id'] + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');"></div></a>'); $(td).html('<a href="' + page('user', rowData['user_id']) + '"" title="' + rowData['username'] + '"><div class="users-poster-face" style="background-image: url(' + rowData['user_thumb'] + ');">' + inactive + '</div></a>');
} }
}, },
"orderable": false, "orderable": false,
@@ -75,8 +77,8 @@ users_list_table_options = {
"data": "friendly_name", "data": "friendly_name",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') { if (cellData !== null && cellData !== '') {
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '">' + $(td).html('<div class="edit-user-name" data-id="' + rowData['row_id'] + '">' +
'<a href="user?user_id=' + rowData['user_id'] + '">' + cellData + '</a>' + '<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>' +
'<input type="text" class="hidden" value="' + cellData + '">' + '<input type="text" class="hidden" value="' + cellData + '">' +
'</div>'); '</div>');
} else { } else {
@@ -140,7 +142,7 @@ users_list_table_options = {
if (rowData['transcode_decision'] === 'transcode') { if (rowData['transcode_decision'] === 'transcode') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Transcode"><i class="fa fa-server fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'copy') { } else if (rowData['transcode_decision'] === 'copy') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-video-camera fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Stream"><i class="fa fa-stream fa-fw"></i></span>';
} else if (rowData['transcode_decision'] === 'direct play') { } else if (rowData['transcode_decision'] === 'direct play') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>'; transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
} }
@@ -157,26 +159,34 @@ users_list_table_options = {
"data":"last_played", "data":"last_played",
"createdCell": function (td, cellData, rowData, row, col) { "createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') { if (cellData !== null && cellData !== '') {
var icon = '';
var icon_title = '';
var parent_info = ''; var parent_info = '';
var media_type = ''; var media_type = '';
var thumb_popover = ''; var thumb_popover = '';
var fallback = (rowData['live']) ? 'poster-live' : 'poster';
if (rowData['media_type'] === 'movie') { if (rowData['media_type'] === 'movie') {
icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-film';
icon_title = (rowData['live']) ? 'Live TV' : 'Movie';
if (rowData['year']) { parent_info = ' (' + rowData['year'] + ')'; } 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>'; media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' 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="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'episode') { } else if (rowData['media_type'] === 'episode') {
if (rowData['parent_media_index'] && rowData['media_index']) { parent_info = ' (S' + rowData['parent_media_index'] + '&middot; E' + rowData['media_index'] + ')'; } icon = (rowData['live']) ? 'fa-broadcast-tower' : 'fa-television';
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Episode"><i class="fa fa-television fa-fw"></i></span>'; icon_title = (rowData['live']) ? 'Live TV' : 'Episode';
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>' if (!isNaN(parseInt(rowData['parent_media_index'])) && !isNaN(parseInt(rowData['media_index']))) { parent_info = ' (S' + rowData['parent_media_index'] + ' &middot; E' + rowData['media_index'] + ')'; }
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); else if (rowData['live'] && rowData['originally_available_at']) { parent_info = ' (' + rowData['originally_available_at'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="' + icon_title + '"><i class="fa ' + icon + ' fa-fw"></i></span>';
thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 450, null, null, null, fallback) + '" data-height="120" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;" >' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type'] === 'track') { } else if (rowData['media_type'] === 'track') {
if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; } if (rowData['parent_title']) { parent_info = ' (' + rowData['parent_title'] + ')'; }
media_type = '<span class="media-type-tooltip" data-toggle="tooltip" title="Track"><i class="fa fa-music fa-fw"></i></span>'; 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">' + cellData + parent_info + '</span>' thumb_popover = '<span class="thumb-tooltip" data-toggle="popover" data-img="' + page('pms_image_proxy', rowData['thumb'], rowData['rating_key'], 300, 300, null, null, null, 'cover') + '" data-height="80" data-width="80">' + cellData + parent_info + '</span>';
$(td).html('<div class="history-title"><a href="info?source=history&rating_key=' + rowData['rating_key'] + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>'); $(td).html('<div class="history-title"><a href="' + page('info', rowData['rating_key'], rowData['guid'], true, rowData['live']) + '"><div style="float: left;">' + media_type + '&nbsp;' + thumb_popover + '</div></a></div>');
} else if (rowData['media_type']) { } else if (rowData['media_type']) {
$(td).html('<a href="info?rating_key=' + rowData['rating_key'] + '">' + cellData + '</a>'); $(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
} }
} else { } else {
$(td).html('n/a'); $(td).html('n/a');
@@ -246,10 +256,10 @@ users_list_table_options = {
}, },
"rowCallback": function (row, rowData) { "rowCallback": function (row, rowData) {
if ($.inArray(rowData['user_id'], users_to_delete) !== -1) { if ($.inArray(rowData['user_id'], users_to_delete) !== -1) {
$(row).find('button.delete-user[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); $(row).find('button.delete-user[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
} }
if ($.inArray(rowData['user_id'], users_to_purge) !== -1) { if ($.inArray(rowData['user_id'], users_to_purge) !== -1) {
$(row).find('button.purge-user[data-id="' + rowData['user_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger'); $(row).find('button.purge-user[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
} }
} }
} }
@@ -260,7 +270,7 @@ $('#users_list_table').on('click', 'td.modal-control', function () {
var rowData = row.data(); var rowData = row.data();
$.get('get_stream_data', { $.get('get_stream_data', {
row_id: rowData['id'], row_id: rowData['history_row_id'],
user: rowData['friendly_name'] user: rowData['friendly_name']
}).then(function (jqXHR) { }).then(function (jqXHR) {
$("#info-modal").html(jqXHR); $("#info-modal").html(jqXHR);
@@ -318,11 +328,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto
var row = users_list_table.row(tr); var row = users_list_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var index_delete = $.inArray(rowData['user_id'], users_to_delete); var index_delete = $.inArray(rowData['row_id'], users_to_delete);
var index_purge = $.inArray(rowData['user_id'], users_to_purge); var index_purge = $.inArray(rowData['row_id'], users_to_purge);
if (index_delete === -1) { if (index_delete === -1) {
users_to_delete.push(rowData['user_id']); users_to_delete.push(rowData['row_id']);
if (index_purge === -1) { if (index_purge === -1) {
tr.find('button.purge-user').click(); tr.find('button.purge-user').click();
} }
@@ -341,11 +351,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto
var row = users_list_table.row(tr); var row = users_list_table.row(tr);
var rowData = row.data(); var rowData = row.data();
var index_delete = $.inArray(rowData['user_id'], users_to_delete); var index_delete = $.inArray(rowData['row_id'], users_to_delete);
var index_purge = $.inArray(rowData['user_id'], users_to_purge); var index_purge = $.inArray(rowData['row_id'], users_to_purge);
if (index_purge === -1) { if (index_purge === -1) {
users_to_purge.push(rowData['user_id']); users_to_purge.push(rowData['row_id']);
} else { } else {
users_to_purge.splice(index_purge, 1); users_to_purge.splice(index_purge, 1);
if (index_delete != -1) { if (index_delete != -1) {

View File

@@ -38,7 +38,7 @@
<th align="left" id="count">Total Movies / TV Shows / Artists</th> <th align="left" id="count">Total Movies / TV Shows / Artists</th>
<th align="left" id="parent_count">Total Seasons / Albums</th> <th align="left" id="parent_count">Total Seasons / Albums</th>
<th align="left" id="child_count">Total Episodes / Tracks</th> <th align="left" id="child_count">Total Episodes / Tracks</th>
<th align="left" id="last_accessed">Last Accessed</th> <th align="left" id="last_accessed">Last Streamed</th>
<th align="left" id="last_played">Last Played</th> <th align="left" id="last_played">Last Played</th>
<th align="left" id="total_plays">Total Plays</th> <th align="left" id="total_plays">Total Plays</th>
<th align="left" id="total_duration">Total Played Duration</th> <th align="left" id="total_duration">Total Played Duration</th>
@@ -116,14 +116,14 @@
}); });
if (libraries_to_delete.length > 0) { if (libraries_to_delete.length > 0) {
$('#libraries-to-delete').prepend('<p>Are you REALLY sure you want to delete the following libraries:</p>') $('#libraries-to-delete').prepend('<p>Are you REALLY sure you want to delete the following libraries:</p>');
for (var i = 0; i < libraries_to_delete.length; i++) { for (var i = 0; i < libraries_to_delete.length; i++) {
$('#libraries-to-delete').append('<li>' + $('div[data-id=' + libraries_to_delete[i] + ']').text() + '</li>'); $('#libraries-to-delete').append('<li>' + $('div[data-id=' + libraries_to_delete[i] + ']').text() + '</li>');
} }
} }
if (libraries_to_purge.length > 0) { if (libraries_to_purge.length > 0) {
$('#libraries-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following libraries:</p>') $('#libraries-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following libraries:</p>');
for (var i = 0; i < libraries_to_purge.length; i++) { for (var i = 0; i < libraries_to_purge.length; i++) {
$('#libraries-to-purge').append('<li>' + $('div[data-id=' + libraries_to_purge[i] + ']').text() + '</li>'); $('#libraries-to-purge').append('<li>' + $('div[data-id=' + libraries_to_purge[i] + ']').text() + '</li>');
} }
@@ -131,33 +131,30 @@
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
libraries_to_delete.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_all_library_history',
url: 'delete_library', type: 'POST',
type: 'POST', data: { row_ids: libraries_to_purge.join(',') },
data: { section_id: row }, cache: false,
cache: false, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "Library history purged";
var msg = "Library deleted"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); libraries_list_table.draw();
} }
});
}); });
libraries_to_purge.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_library',
url: 'delete_all_library_history', type: 'POST',
type: 'POST', data: { row_ids: libraries_to_delete.join(',') },
data: { section_id: row }, cache: false,
cache: false, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "Library deleted";
var msg = "Library history purged"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); libraries_list_table.draw();
} }
});
}); });
libraries_list_table.draw();
}); });
} }
@@ -188,7 +185,7 @@
complete: function (xhr, status) { complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = result.message; var msg = result.message;
if (result.result == 'success') { if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false); showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
libraries_list_table.draw(); libraries_list_table.draw();
} else { } else {

View File

@@ -35,10 +35,14 @@ DOCUMENTATION :: END
<%def name="body()"> <%def name="body()">
% if data: % if data:
<%
from plexpy.common import LIVE_TV_SECTION_ID
from plexpy.helpers import page
%>
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
% if data['library_art']: % if data['library_art']:
<div class="art-face" style="background-image:url(pms_image_proxy?img=${data['library_art']}&width=1920&height=1080)"></div> <div class="art-face" style="background-image:url(${page('pms_image_proxy', data['library_art'], None, 1920, 1080)})"></div>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
<span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span> <span class="overlay-refresh-image info-art" title="Refresh background image"><i class="fa fa-refresh refresh_pms_image"></i></span>
% endif % endif
@@ -57,10 +61,22 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-back"> <div class="table-card-back">
<div class="user-info-wrapper"> <div class="user-info-wrapper">
% if data['library_thumb'][:4] == 'http' or data['library_thumb'][:10] == 'interfaces': % if data['library_thumb'].startswith('http'):
<div class="library-info-poster-face" style="background-image: url(${data['library_thumb']});"></div> <div class="library-info-poster-face" style="background-image: url(${page('pms_image_proxy', data['library_thumb'], None, 80, 80)});">
% if not data['is_active']:
<span class="inactive-library-tooltip" data-toggle="tooltip" title="Library not on Plex server">
<i class="fa fa-2x fa-exclamation-triangle"></i>
</span>
% endif
</div>
% else: % else:
<div class="library-info-poster-face svg-icon library-${data['section_type']}"></div> <div class="library-info-poster-face svg-icon library-${data['section_type']}">
% if not data['is_active']:
<span class="inactive-library-tooltip" data-toggle="tooltip" title="Library not on Plex server">
<i class="fa fa-2x fa-exclamation-triangle"></i>
</span>
% endif
</div>
% endif % endif
<div class="user-info-username"> <div class="user-info-username">
<span class="set-username">${data['section_name']}</span> <span class="set-username">${data['section_name']}</span>
@@ -75,8 +91,10 @@ DOCUMENTATION :: END
<li class="active"><a href="#tabs-profile" role="tab" data-toggle="tab">Profile</a></li> <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> <li><a id="history-tab-btn" href="#tabs-history" role="tab" data-toggle="tab">History</a></li>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
% if data['section_id'] != LIVE_TV_SECTION_ID:
<li><a id="media-info-tab-btn" href="#tabs-mediainfo" role="tab" 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 % endif
% endif
</ul> </ul>
</div> </div>
</div> </div>
@@ -143,6 +161,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% if data['section_id'] != LIVE_TV_SECTION_ID:
<div class="container-fluid"> <div class="container-fluid">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@@ -168,6 +187,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% endif
</div> </div>
<div role="tabpanel" class="tab-pane" id="tabs-history"> <div role="tabpanel" class="tab-pane" id="tabs-history">
<div class="container-fluid"> <div class="container-fluid">
@@ -205,6 +225,7 @@ DOCUMENTATION :: END
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="player">Player</th> <th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -230,9 +251,9 @@ DOCUMENTATION :: END
% else: % else:
<div id="get_file_sizes_message" style="text-align: center; margin-top: 20px; display: none;"> <div id="get_file_sizes_message" style="text-align: center; margin-top: 20px; display: none;">
% endif % endif
<i class="fa fa-refresh fa-spin"></i> Tautulli is calculating the file sizes for the library's media info. This could take a few minutes depending on the size of your library. <i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is calculating the file sizes for the library's media info. This could take a few minutes depending on the size of your library.
<br /> <br />
You may leave this page and come back later. You may leave this page and check back later.
</div> </div>
<div class='table-card-header'> <div class='table-card-header'>
<div class="header-bar"> <div class="header-bar">
@@ -347,6 +368,7 @@ DOCUMENTATION :: END
<script src="${http_root}js/dataTables.bootstrap.min.js"></script> <script src="${http_root}js/dataTables.bootstrap.min.js"></script>
<script src="${http_root}js/dataTables.bootstrap.pagination.js"></script> <script src="${http_root}js/dataTables.bootstrap.pagination.js"></script>
% if data: % if data:
<% from plexpy.common import LIVE_TV_SECTION_ID %>
<script> <script>
% if str(data['section_id']).isdigit(): % if str(data['section_id']).isdigit():
var section_id = ${data['section_id']}; var section_id = ${data['section_id']};
@@ -385,7 +407,7 @@ DOCUMENTATION :: END
}; };
history_table = $('#history_table-SID-${data["section_id"]}').DataTable(history_table_options); 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] }); var colvis = new $.fn.dataTable.ColVis(history_table, { buttonText: '<i class="fa fa-columns"></i> Select columns', buttonClass: 'btn btn-dark', exclude: [0, 12] });
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-SID-${data["section_id"]}', history_table); clearSearchButton('history_table-SID-${data["section_id"]}', history_table);
@@ -401,6 +423,8 @@ DOCUMENTATION :: END
history_table.draw(); history_table.draw();
}); });
$(".inactive-library-tooltip").tooltip();
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
function loadMediaInfoTable() { function loadMediaInfoTable() {
// Build media info table // Build media info table
@@ -461,19 +485,17 @@ DOCUMENTATION :: END
$('#deleteCount').text(history_to_delete.length); $('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_history_rows',
url: 'delete_history_rows', type: 'POST',
type: 'POST', data: { row_ids: history_to_delete.join(',') },
data: { row_id: row }, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "History deleted";
var msg = "History deleted"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); history_table.draw();
} }
});
}); });
history_table.draw();
}); });
} }
@@ -525,7 +547,9 @@ DOCUMENTATION :: END
} }
recentlyWatched(); recentlyWatched();
% if data['section_id'] != LIVE_TV_SECTION_ID:
recentlyAdded(); recentlyAdded();
% endif
function highlightWatchedScrollerButton() { function highlightWatchedScrollerButton() {
var scroller = $("#recently-watched-row-scroller"); var scroller = $("#recently-watched-row-scroller");

View File

@@ -31,6 +31,9 @@ DOCUMENTATION :: END
</%doc> </%doc>
% if data: % if data:
<%
from plexpy.helpers import page
%>
<div class="dashboard-recent-media-row"> <div class="dashboard-recent-media-row">
<div id="recently-added-row-scroller" style="left: 0;"> <div id="recently-added-row-scroller" style="left: 0;">
<ul class="dashboard-recent-media list-unstyled"> <ul class="dashboard-recent-media list-unstyled">
@@ -38,19 +41,19 @@ DOCUMENTATION :: END
<li> <li>
% if item['media_type'] == 'episode' or item['media_type'] == 'movie': % if item['media_type'] == 'episode' or item['media_type'] == 'movie':
% if item['media_type'] == 'movie': % if item['media_type'] == 'movie':
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}"> <a href="${page('info', item['rating_key'])}" title="${item['title']}">
% elif item['media_type'] == 'episode': % elif item['media_type'] == 'episode':
<a href="info?rating_key=${item['rating_key']}" title="${item['grandparent_title']}"> <a href="${page('info', item['rating_key'])}" title="${item['grandparent_title']}">
% endif % endif
<div class="dashboard-recent-media-poster"> <div class="dashboard-recent-media-poster">
% if item['media_type'] == 'episode': % if item['media_type'] == 'episode':
% if item['parent_thumb']: % if item['parent_thumb']:
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['parent_thumb']}&width=300&height=450&fallback=poster);"> <div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['parent_thumb'], item['parent_rating_key'], 300, 450, fallback='poster')});">
% else: % else:
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['grandparent_thumb']}&width=300&height=450&fallback=poster);"> <div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['grandparent_thumb'], item['grandparent_rating_key'], 300, 450, fallback='poster')});">
% endif % endif
% elif item['media_type'] == 'movie': % elif item['media_type'] == 'movie':
<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-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
% endif % endif
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
@@ -68,27 +71,27 @@ DOCUMENTATION :: END
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
% if item['media_type'] == 'episode': % if item['media_type'] == 'episode':
<h3> <h3>
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a> <a href="${page('info', item['grandparent_rating_key'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a href="info?rating_key=${item['parent_rating_key']}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a> <a href="${page('info', item['parent_rating_key'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
&middot; <a href="info?rating_key=${item['rating_key']}" title="Episode ${item['media_index']}">E${item['media_index']}</a> &middot; <a href="${page('info', item['rating_key'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
</h3> </h3>
% elif item['media_type'] == 'movie': % elif item['media_type'] == 'movie':
<h3> <h3>
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted">${item['year']}</h3> <h3 class="text-muted">${item['year']}</h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
% endif % endif
</div> </div>
% elif item['media_type'] == 'album': % elif item['media_type'] == 'album':
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}"> <a href="${page('info', item['rating_key'])}" title="${item['title']}">
<div class="dashboard-recent-media-cover"> <div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"> <div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script> <script>
@@ -100,10 +103,10 @@ DOCUMENTATION :: END
</div> </div>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3> <h3>
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a> <a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a class="text-muted" href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a class="text-muted" href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
</div> </div>

View File

@@ -25,6 +25,8 @@ DOCUMENTATION :: END
% if data: % if data:
<% <%
from plexpy.helpers import page
types = ('movie', 'show', 'artist', 'photo') types = ('movie', 'show', 'artist', 'photo')
headers = {'movie': ('Movie Libraries', ('Movies', '', '')), headers = {'movie': ('Movie Libraries', ('Movies', '', '')),
'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')), 'show': ('TV Show Libraries', ('Shows', 'Seasons', 'Episodes')),
@@ -33,10 +35,17 @@ DOCUMENTATION :: END
%> %>
% for section_type in types: % for section_type in types:
% if section_type in data: % if section_type in data:
<%
row0 = data[section_type][0]
%>
<div class="dashboard-stats-instance" id="library-stats-instance-${section_type}" data-section_type="${section_type}"> <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-container">
<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-background-${section_type}" class="dashboard-stats-background" style="background-image: url(${page('pms_image_proxy', row0['art'], None, 500, 280, 40, '282828', 3, fallback='art')});">
% if row0['thumb'].startswith('http'):
<div id="library-stats-thumb-${section_type}" class="dashboard-stats-flat hidden-xs" style="background-image: url(${page('pms_image_proxy', row0['thumb'], None, 80, 80)});"></div>
% else:
<div id="library-stats-thumb-${section_type}" class="dashboard-stats-flat svg-icon library-${section_type} hidden-xs"></div> <div id="library-stats-thumb-${section_type}" class="dashboard-stats-flat svg-icon library-${section_type} hidden-xs"></div>
% endif
<div class="dashboard-stats-info-container"> <div class="dashboard-stats-info-container">
<div id="library-stats-title-${section_type}" class="dashboard-stats-info-title"> <div id="library-stats-title-${section_type}" class="dashboard-stats-info-title">
<h4>${headers[section_type][0]}</h4> <h4>${headers[section_type][0]}</h4>
@@ -46,10 +55,11 @@ DOCUMENTATION :: END
<div class="dashboard-stats-info scoller-content"> <div class="dashboard-stats-info scoller-content">
<ul class="list-unstyled dashboard-stats-info-list"> <ul class="list-unstyled dashboard-stats-info-list">
% for section in data[section_type]: % for section in data[section_type]:
<li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}"> <li class="dashboard-stats-info-item ${'expanded' if loop.index == 0 else ''}" data-stat_id="${section_type}"
data-art="${section.get('art')}" data-thumb="${section.get('thumb')}">
<div class="sub-list">${loop.index + 1}</div> <div class="sub-list">${loop.index + 1}</div>
<div class="sub-value"> <div class="sub-value">
<a href="library?section_id=${section['section_id']}" title="${section['section_name']}"> <a href="${page('library', section['section_id'])}" title="${section['section_name']}">
${section['section_name']} ${section['section_name']}
</a> </a>
</div> </div>

View File

@@ -19,16 +19,17 @@ DOCUMENTATION :: END
</%doc> </%doc>
% if data: % if data:
<% from plexpy.helpers import page %>
% for a in data: % for a in data:
<ul class="list-unstyled"> <ul class="list-unstyled">
<div class="user-player-instance"> <div class="user-player-instance">
<li> <li>
% if a['user_id']: % if a['user_id']:
<a href="user?user_id=${a['user_id']}" title="${a['friendly_name']}"> <a href="${page('user', a['user_id'])}" title="${a['username']}">
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div> <div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>
</a> </a>
<div class=" user-player-instance-name"> <div class=" user-player-instance-name">
<a href="user?user_id=${a['user_id']}" title="${a['friendly_name']}">${a['friendly_name']}</a> <a href="${page('user', a['user_id'])}" title="${a['username']}">${a['friendly_name']}</a>
</div> </div>
% else: % else:
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div> <div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>

View File

@@ -24,7 +24,7 @@
<!-- ICONS --> <!-- ICONS -->
<!-- Android --> <!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5"> <link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<meta name="theme-color" content="#282a2d"> <meta name="theme-color" content="#282a2d">
<!-- Apple --> <!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5"> <link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">
@@ -150,6 +150,7 @@
token: token, token: token,
remember_me: remember_me remember_me: remember_me
}; };
var x_plex_headers = getPlexHeaders();
data = $.extend(data, x_plex_headers); data = $.extend(data, x_plex_headers);
$.ajax({ $.ajax({
@@ -182,4 +183,4 @@
} }
</script> </script>
</body> </body>
</html> </html>

View File

@@ -33,7 +33,7 @@
<label for="friendly_name">OneSignal Device ID</label> <label for="friendly_name">OneSignal Device ID</label>
<div class="row"> <div class="row">
<div class="col-md-8"> <div class="col-md-8">
<input type="text" class="form-control" id="device_id" value="${device['device_id']}" size="30" readonly> <input type="text" class="form-control" id="onesignal_id" value="${device['onesignal_id'] or ''}" size="30" readonly>
</div> </div>
</div> </div>
<p class="help-block">Your OneSignal device ID for notifications.</p> <p class="help-block">Your OneSignal device ID for notifications.</p>

View File

@@ -13,7 +13,11 @@ DOCUMENTATION :: END
% for device in sorted(devices_list, key=lambda k: k['device_name']): % for device in sorted(devices_list, key=lambda k: k['device_name']):
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}"> <li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
<span> <span>
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span> % if device['official']:
<span class="toggle-left"><i class="fa fa-lg fa-fw fa-mobile"></i></span>
% else:
<span class="toggle-left officail-tooltip" data-toggle="tooltip" data-placement="top" title="Unofficial or Unknown App"><i class="fa fa-lg fa-fw fa-exclamation-triangle"></i></span>
% endif
${device['friendly_name'] or device['device_name']} &nbsp;<span class="friendly_name">(${device['id']})</span> ${device['friendly_name'] or device['device_name']} &nbsp;<span class="friendly_name">(${device['id']})</span>
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
<span class="toggle-right friendly_name" id="device-last_seen-${device['id']}"> <span class="toggle-right friendly_name" id="device-last_seen-${device['id']}">
@@ -117,6 +121,7 @@ DOCUMENTATION :: END
}); });
$('#api_qr_address').change(function () { $('#api_qr_address').change(function () {
this.value = $.trim(this.value);
var url = $(this).val(); var url = $(this).val();
checkQRAddress(url); checkQRAddress(url);
@@ -138,4 +143,6 @@ DOCUMENTATION :: END
} }
verifiedDevice = true; verifiedDevice = true;
}) })
$('.officail-tooltip').tooltip();
</script> </script>

View File

@@ -123,7 +123,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}"> <select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()): % for key, value in sorted(item['select_options'].items()):
% if key == item['value']: % if key == item['value']:
<option value="${key}" selected>${value}</option> <option value="${key}" selected>${value}</option>
% else: % else:
@@ -144,7 +144,7 @@
<option value="select-all">Select All</option> <option value="select-all">Select All</option>
<option value="remove-all">Remove All</option> <option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict): % if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems(): % for section, options in item['select_options'].items():
<optgroup label="${section}"> <optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()): % for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option> <option value="${option['value']}">${option['text']}</option>
@@ -271,7 +271,7 @@
</div> </div>
<p class="help-block"> <p class="help-block">
Select an existing notification agent where the subject and body text will be sent.<br> 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. Note: Self-hosted newsletters must be enabled under <a data-tab-destination="notifications" data-dismiss="modal" data-target="newsletter_self_hosted">Newsletters</a> to include a link to the newsletter.
</p> </p>
</div> </div>
<div id="newsletter-email-config"> <div id="newsletter-email-config">
@@ -325,7 +325,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}"> <select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()): % for key, value in sorted(item['select_options'].items()):
% if key == item['value']: % if key == item['value']:
<option value="${key}" selected>${value}</option> <option value="${key}" selected>${value}</option>
% else: % else:
@@ -346,7 +346,7 @@
<option value="select-all">Select All</option> <option value="select-all">Select All</option>
<option value="remove-all">Remove All</option> <option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict): % if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems(): % for section, options in item['select_options'].items():
<optgroup label="${section}"> <optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()): % for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option> <option value="${option['value']}">${option['text']}</option>
@@ -584,6 +584,7 @@
var $email_selectors = $('#newsletter_email_to, #newsletter_email_cc, #newsletter_email_bcc').selectize({ var $email_selectors = $('#newsletter_email_to, #newsletter_email_cc, #newsletter_email_bcc').selectize({
plugins: ['remove_button'], plugins: ['remove_button'],
maxItems: null, maxItems: null,
searchField: ['text', 'value'],
render: { render: {
item: function(item, escape) { item: function(item, escape) {
return '<div>' + return '<div>' +

View File

@@ -1,5 +1,5 @@
<% <%
import urllib from six.moves.urllib.parse import urlencode
%> %>
<!doctype html> <!doctype html>
@@ -8,6 +8,9 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Tautulli - ${title} | ${server_name}</title> <title>Tautulli - ${title} | ${server_name}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.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">
<link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet"> <link href="${http_root}css/tautulli.css${cache_param}" rel="stylesheet">
<style> <style>
* { * {
@@ -32,7 +35,7 @@
<script> <script>
$(document).ready(function () { $(document).ready(function () {
var frame = $('<iframe></iframe>', { var frame = $('<iframe></iframe>', {
src: 'real_newsletter?${urllib.urlencode(kwargs) | n}', src: 'real_newsletter?${urlencode(kwargs) | n}',
frameborder: '0', frameborder: '0',
style: 'display: none; height: 100vh; width: 100vw;' style: 'display: none; height: 100vh; width: 100vw;'
}); });

View File

@@ -9,7 +9,7 @@ Version: 0.1
DOCUMENTATION :: END DOCUMENTATION :: END
</%doc> </%doc>
<% from plexpy.newsletter_handler import NEWSLETTER_SCHED %> <% from plexpy import newsletter_handler %>
<ul class="stacked-configs list-unstyled"> <ul class="stacked-configs list-unstyled">
% for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])): % for newsletter in sorted(newsletters_list, key=lambda k: (k['agent_label'], k['friendly_name'], k['id'])):
<li class="newsletter-agent pointer" data-id="${newsletter['id']}"> <li class="newsletter-agent pointer" data-id="${newsletter['id']}">
@@ -22,8 +22,8 @@ DOCUMENTATION :: END
% endif % endif
<span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span> <span class="toggle-right"><i class="fa fa-lg fa-fw fa-cog"></i></span>
<span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}"> <span class="toggle-right friendly_name" id="newsletter-next_run-${newsletter['id']}">
% if NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])): % if newsletter_handler.NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])):
<% job = NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %> <% job = newsletter_handler.NEWSLETTER_SCHED.get_job('newsletter-{}'.format(newsletter['id'])) %>
<script> <script>
$("#newsletter-next_run-${newsletter['id']}").text(moment("${job.next_run_time}", "YYYY-MM-DD HH:mm:ssZ").fromNow()) $("#newsletter-next_run-${newsletter['id']}").text(moment("${job.next_run_time}", "YYYY-MM-DD HH:mm:ssZ").fromNow())
</script> </script>

View File

@@ -1,9 +1,9 @@
% if notifier: % if notifier:
<%! <%
import json import json
from plexpy import notifiers, users from plexpy import notifiers, users
from plexpy.helpers import checked from plexpy.helpers import checked
available_notification_actions = notifiers.available_notification_actions() available_notification_actions = notifiers.available_notification_actions(agent_id=notifier['agent_id'])
user_emails = [{'user': u['friendly_name'] or u['username'], 'email': u['email']} for u in users.Users().get_users() if u['email']] 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']) sorted(user_emails, key=lambda u: u['user'])
@@ -25,7 +25,7 @@
<li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">Arguments</a></li> <li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">Arguments</a></li>
% elif notifier['agent_name'] == 'webhook': % elif notifier['agent_name'] == 'webhook':
<li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">Data</a></li> <li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">Data</a></li>
% else: % elif notifier['agent_name'] != 'plexmobileapp':
<li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">Text</a></li> <li role="presentation"><a href="#tabs-notify_text" aria-controls="tabs-notify_text" role="tab" data-toggle="tab">Text</a></li>
% endif % endif
<li role="presentation"><a href="#tabs-test_notifications" aria-controls="tabs-test_notifications" role="tab" data-toggle="tab">Test Notifications</a></li> <li role="presentation"><a href="#tabs-test_notifications" aria-controls="tabs-test_notifications" role="tab" data-toggle="tab">Test Notifications</a></li>
@@ -49,7 +49,16 @@
<label for="${item['name']}">${item['label']}</label> <label for="${item['name']}">${item['label']}</label>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
% if notifier['agent_name'] == 'scripts' and item['name'] == 'scripts_script_folder':
<div class="input-group">
<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 ''}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="${item['name']}_browse" data-toggle="browse" data-filter=".folderonly" data-target="#${item['name']}">Browse</button>
</span>
</div>
% else:
<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 ''}> <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 ''}>
% endif
</div> </div>
</div> </div>
<p class="help-block">${item['description'] | n}</p> <p class="help-block">${item['description'] | n}</p>
@@ -88,7 +97,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}"> <select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()): % for key, value in sorted(item['select_options'].items()):
% if key == item['value']: % if key == item['value']:
<option value="${key}" selected>${value}</option> <option value="${key}" selected>${value}</option>
% else: % else:
@@ -109,7 +118,7 @@
<option value="select-all">Select All</option> <option value="select-all">Select All</option>
<option value="remove-all">Remove All</option> <option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict): % if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems(): % for section, options in item['select_options'].items():
<optgroup label="${section}"> <optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()): % for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option> <option value="${option['value']}">${option['text']}</option>
@@ -148,7 +157,7 @@
<div class="col-md-12"> <div class="col-md-12">
<label>Notification Triggers</label> <label>Notification Triggers</label>
<p class="help-block"> <p class="help-block">
Select items that will trigger a notification for this ${notifier['agent_label']} notifiation agent. Select items that will trigger a notification for this ${notifier['agent_label']} notification agent.
</p> </p>
% for action in available_notification_actions: % for action in available_notification_actions:
<div class="checkbox"> <div class="checkbox">
@@ -211,7 +220,7 @@
% for action in available_notification_actions: % for action in available_notification_actions:
<li> <li>
<div class="link"> <div class="link">
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>&nbsp; <span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>
${action['label']} ${action['label']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span> <span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div> </div>
@@ -237,12 +246,17 @@
% for action in available_notification_actions: % for action in available_notification_actions:
<li> <li>
<div class="link"> <div class="link">
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>&nbsp; <span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>
${action['label']} ${action['label']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span> <span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div> </div>
<ul class="submenu"> <ul class="submenu">
<li> <li>
<div class="form-group">
<label for="${action['name']}_subject">JSON Headers</label>
<textarea class="form-control" id="${action['name']}_subject" name="${action['name']}_subject" data-parsley-trigger="change" data-autoresize required>${notifier['notify_text'][action['name']]['subject']}</textarea>
<p class="help-block">Set custom JSON headers.</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="${action['name']}_body">JSON Data</label> <label for="${action['name']}_body">JSON Data</label>
<textarea class="form-control" id="${action['name']}_body" name="${action['name']}_body" data-parsley-trigger="change" data-autoresize required>${notifier['notify_text'][action['name']]['body']}</textarea> <textarea class="form-control" id="${action['name']}_body" name="${action['name']}_body" data-parsley-trigger="change" data-autoresize required>${notifier['notify_text'][action['name']]['body']}</textarea>
@@ -263,7 +277,7 @@
% for action in available_notification_actions: % for action in available_notification_actions:
<li> <li>
<div class="link"> <div class="link">
<span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>&nbsp; <span class="toggle-left"><i class="fa ${action['icon']} fa-fw"></i></span>
${action['label']} ${action['label']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span> <span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div> </div>
@@ -308,7 +322,7 @@
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<select class="form-control" id="test_script" name="test_script"> <select class="form-control" id="test_script" name="test_script">
% for key, value in sorted(notifier['config_options'][2]['select_options'].iteritems()): % for key, value in sorted(notifier['config_options'][2]['select_options'].items()):
<option value="${key}">${value}</option> <option value="${key}">${value}</option>
% endfor % endfor
</select> </select>
@@ -326,6 +340,15 @@
<p class="help-block">Set custom arguments passed to the script.</p> <p class="help-block">Set custom arguments passed to the script.</p>
</div> </div>
% elif notifier['agent_name'] == 'webhook': % elif notifier['agent_name'] == 'webhook':
<div class="form-group">
<label for="test_subject">JSON Headers</label>
<div class="row">
<div class="col-md-12">
<textarea class="form-control" id="test_subject" name="test_subject" data-autoresize></textarea>
</div>
</div>
<p class="help-block">Set custom JSON headers sent to the webhook.</p>
</div>
<div class="form-group"> <div class="form-group">
<label for="test_body">JSON Data</label> <label for="test_body">JSON Data</label>
<div class="row"> <div class="row">
@@ -471,7 +494,7 @@
'<div class="form-group">' + '<div class="form-group">' +
'<label>Warning</label>' + '<label>Warning</label>' +
'<p class="help-block" style="color: #eb8600;">Facebook requires HTTPS for authorization. ' + '<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>' + 'Please enable HTTPS for Tautulli under <a data-tab-destination="web_interface" data-dismiss="modal" data-target="enable_https">Web Interface</a>.</p>' +
'</div>' '</div>'
); );
$('#facebook_redirect_uri').val('HTTPS not enabled'); $('#facebook_redirect_uri').val('HTTPS not enabled');
@@ -566,6 +589,7 @@
var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({ var $email_selectors = $('#email_to, #email_cc, #email_bcc').selectize({
plugins: ['remove_button'], plugins: ['remove_button'],
maxItems: null, maxItems: null,
searchField: ['text', 'value'],
render: { render: {
item: function(item, escape) { item: function(item, escape) {
return '<div>' + return '<div>' +
@@ -669,6 +693,15 @@
pushoverPriority(); pushoverPriority();
}); });
% elif notifier['agent_name'] == 'plexmobileapp':
var $plexmobileapp_user_ids = $('#plexmobileapp_user_ids').selectize({
plugins: ['remove_button'],
maxItems: null,
create: true
});
var plexmobileapp_user_ids = $plexmobileapp_user_ids[0].selectize;
plexmobileapp_user_ids.setValue(${json.dumps(next((c['value'] for c in notifier['config_options'] if c['name'] == 'plexmobileapp_user_ids'), [])) | n});
% endif % endif
function validateLogic() { function validateLogic() {
@@ -829,10 +862,8 @@
PNotify.prototype.options.hide = true; PNotify.prototype.options.hide = true;
PNotify.prototype.options.delay = $('#browser_auto_hide_delay').val() * 1000; PNotify.prototype.options.delay = $('#browser_auto_hide_delay').val() * 1000;
} }
var notification = new PNotify({ displayPNotify($('#test_subject').val(), $('#test_body').val());
title: $('#test_subject').val(), showMsg('<i class="fa fa-check"></i> Notification sent.', false, true, 5000);
text: $('#test_body').val()
});
} }
} }

View File

@@ -8,7 +8,12 @@
% if text: % if text:
% for item in text: % for item in text:
<div style="padding-bottom: 10px;"> <div style="padding-bottom: 10px;">
<h4>${item['media_type'].capitalize()}</h4> <h4>
${item['media_type'].capitalize()}
% if item['media_type'] != 'server':
<span class="inline-pre">&lt;${item['media_type']}&gt;&lt;/${item['media_type']}&gt;</span> tags
% endif
</h4>
% if agent != 'webhook': % if agent != 'webhook':
<pre>${item['subject']}</pre> <pre>${item['subject']}</pre>
% endif % endif

View File

@@ -0,0 +1,67 @@
<%
import datetime
import plexpy
from plexpy import activity_handler, helpers
if queue == 'active sessions':
filter_key = 'session_key-'
title_format = '{2} / {1} ({0})'
title_key = title_format.format('Session Key', 'Title', 'User')
description = 'Queue to flush stuck active sessions to the database.'
else:
filter_key = 'rating_key-'
title_format = '{1} ({0})'
title_key = title_format.format('Rating Key', 'Title')
description = 'Queue to flush recently added items to the database and send notifications if enabled.'
scheduled_jobs = [j.id for j in activity_handler.ACTIVITY_SCHED.get_jobs() if j.id.startswith(filter_key)]
%>
<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">${queue.title()} Queue</h4>
</div>
<div class="modal-body">
<p class="help-block">
${description}
</p>
<table class="activity-queue">
<thead>
<tr>
<th>
${title_key}
</th>
<th>
Next Flush In
</th>
<th>
Next Flush Time
</th>
</tr>
</thead>
<tbody>
% if scheduled_jobs:
% for job in scheduled_jobs:
<%
sched_job = activity_handler.ACTIVITY_SCHED.get_job(job)
now = datetime.datetime.now(sched_job.next_run_time.tzinfo)
%>
<tr>
<td><strong>${title_format.format(*sched_job.args)}</strong></td>
<td>${helpers.format_timedelta_Hms(sched_job.next_run_time - now)}</td>
<td>${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')}</td>
</tr>
% endfor
% else:
<tr>
<td colspan="3" style="text-align: center;"><i class="fa fa-check"></i>&nbsp; Nothing in the ${queue} queue</td>
</tr>
% endif
</tbody>
</table>
</div>
<div class="modal-footer">
</div>
</div>
</div>

View File

@@ -31,6 +31,9 @@ DOCUMENTATION :: END
</%doc> </%doc>
% if data != None: % if data != None:
<%
from plexpy.helpers import cast_to_int, page
%>
% if data: % if data:
<div class="dashboard-recent-media-row"> <div class="dashboard-recent-media-row">
<div id="recently-added-row-scroller" style="left: 0;"> <div id="recently-added-row-scroller" style="left: 0;">
@@ -39,9 +42,9 @@ DOCUMENTATION :: END
<div class="dashboard-recent-media-instance"> <div class="dashboard-recent-media-instance">
<li data-type="${item['media_type']}"> <li data-type="${item['media_type']}">
% if item['media_type'] == 'movie': % if item['media_type'] == 'movie':
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}"> <a href="${page('info', item['rating_key'])}" title="${item['title']}">
<div class="dashboard-recent-media-poster"> <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-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script> <script>
@@ -57,15 +60,15 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3> <h3>
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted">${item['year']}</h3> <h3 class="text-muted">${item['year']}</h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
</div> </div>
% elif item['media_type'] == 'show': % elif item['media_type'] == 'show':
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}"> <a href="${page('info', item['rating_key'])}" title="${item['title']}">
<div class="dashboard-recent-media-poster"> <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-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script> <script>
@@ -81,17 +84,21 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3> <h3>
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
${item['child_count']} Seasons ${item['child_count']} Season${'s' if cast_to_int(item['child_count']) > 1 else ''}
</h3> </h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
</div> </div>
% elif item['media_type'] == 'season': % elif item['media_type'] == 'season':
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}"> <a href="${page('info', item['rating_key'])}" title="${item['parent_title']}">
<div class="dashboard-recent-media-poster"> <div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['thumb'] or item['parent_thumb']}&width=300&height=450&fallback=poster);"> % if item['thumb']:
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
% else:
<div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['parent_thumb'], item['parent_rating_key'], 300, 450, fallback='poster')});">
% endif
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script> <script>
@@ -107,17 +114,17 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3> <h3>
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a> <a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a class="text-muted" href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a class="text-muted" href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
</div> </div>
% elif item['media_type'] == 'episode': % elif item['media_type'] == 'episode':
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}"> <a href="${page('info', item['rating_key'])}" title="${item['title']}">
<div class="dashboard-recent-media-poster"> <div class="dashboard-recent-media-poster">
<div class="dashboard-recent-media-poster-face" style="background-image: url(pms_image_proxy?img=${item['grandparent_thumb']}&width=300&height=450&fallback=poster);"> <div class="dashboard-recent-media-poster-face" style="background-image: url(${page('pms_image_proxy', item['grandparent_thumb'], item['grandparent_rating_key'], 300, 450, fallback='poster')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script> <script>
@@ -133,21 +140,21 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3> <h3>
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a> <a href="${page('info', item['grandparent_rating_key'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a href="info?rating_key=${item['parent_rating_key']}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a> <a href="${page('info', item['parent_rating_key'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
&middot; &middot;
<a href="info?rating_key=${item['rating_key']}" title="Episode ${item['media_index']}">E${item['media_index']}</a> <a href="${page('info', item['rating_key'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
</h3> </h3>
</div> </div>
% elif item['media_type'] == 'album': % elif item['media_type'] == 'album':
<a href="info?rating_key=${item['rating_key']}" title="${item['parent_title']}"> <a href="${page('info', item['rating_key'])}" title="${item['parent_title']}">
<div class="dashboard-recent-media-cover"> <div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"> <div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}"> <div class="dashboard-recent-media-overlay-text" id="added_at-${item['rating_key']}">
<script> <script>
@@ -163,14 +170,14 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3> <h3>
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a> <a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a class="text-muted" href="info?rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a class="text-muted" href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
</div> </div>
% endif % endif
</li> </li>
</div> </div>
% endfor % endfor

View File

@@ -10,9 +10,9 @@ DOCUMENTATION :: END
</%doc> </%doc>
<%! <%!
import arrow import datetime
import plexpy import plexpy
from plexpy import common from plexpy import common, helpers
scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()] scheduled_jobs = [j.id for j in plexpy.SCHED.get_jobs()]
%> %>
@@ -28,23 +28,28 @@ DOCUMENTATION :: END
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
% for job in common.SCHEDULER_LIST: % for job, job_type in common.SCHEDULER_LIST.items():
% if job in scheduled_jobs: % if job in scheduled_jobs:
<% <%
sched_job = plexpy.SCHED.get_job(job) sched_job = plexpy.SCHED.get_job(job)
run_interval = arrow.get(str(sched_job.trigger.interval), ['H:mm:ss', 'HH:mm:ss']) now = datetime.datetime.now(sched_job.next_run_time.tzinfo)
next_run_interval = arrow.get(sched_job.next_run_time).timestamp - arrow.now().timestamp
%> %>
<tr> <tr>
<td>${sched_job.id}</td> <td>${sched_job.id}</td>
<td><i class="fa fa-sm fa-fw fa-check"></i> Active</td> <td><i class="fa fa-sm fa-fw fa-check"></i> Active</td>
<td>${arrow.get(run_interval).format('HH:mm:ss')}</td> <td>${helpers.format_timedelta_Hms(sched_job.trigger.interval)}</td>
<td>${arrow.get(next_run_interval).format('HH:mm:ss')}</td> <td>${helpers.format_timedelta_Hms(sched_job.next_run_time - now)}</td>
<td>${arrow.get(sched_job.next_run_time).format('YYYY-MM-DD HH:mm:ss')}</td> <td>${sched_job.next_run_time.astimezone(plexpy.SYS_TIMEZONE).strftime('%Y-%m-%d %H:%M:%S')}</td>
</tr> </tr>
% elif job in ('Check for server response', 'Check for active sessions', 'Check for recently added items') and plexpy.WS_CONNECTED: % elif job_type == 'websocket' and plexpy.WS_CONNECTED:
<tr> <tr>
% if job == 'Check for active sessions':
<td><a class="queue-modal-link no-highlight" href="#" data-queue="active sessions">${job}</a></td>
% elif job == 'Check for recently added items':
<td><a class="queue-modal-link no-highlight" href="#" data-queue="recently added">${job}</a></td>
% else:
<td>${job}</td> <td>${job}</td>
% endif
<td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td> <td><i class="fa fa-sm fa-fw fa-check"></i> Websocket</td>
<td>N/A</td> <td>N/A</td>
<td>N/A</td> <td>N/A</td>
@@ -61,4 +66,21 @@ DOCUMENTATION :: END
% endif % endif
% endfor % endfor
</tbody> </tbody>
</table> </table>
<script>
$('.queue-modal-link').on('click', function (e) {
e.preventDefault();
$.ajax({
url: 'get_queue_modal',
data: {
queue: $(this).data('queue')
},
cache: false,
async: true,
complete: function(xhr, status) {
$("#queue-modal").html(xhr.responseText);
$('#queue-modal').modal();
}
});
});
</script>

File diff suppressed because it is too large Load Diff

View File

@@ -22,10 +22,10 @@
<div class="modal-body" id="modal-text"> <div class="modal-body" id="modal-text">
<div align="center"> <div align="center">
% if message == "Shutting Down": % if message == "Shutting Down":
<h3><i class="fa fa-refresh fa-spin"></i> Tautulli is ${message}.</h3> <h3><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is ${message.lower()}</h3>
<br /> <br />
% else: % else:
<h3><i class="fa fa-refresh fa-spin"></i> Tautulli is ${message}.</h3> <h3><i class="fa fa-refresh fa-spin"></i>&nbsp; Tautulli is ${message.lower()}</h3>
<br /> <br />
<h4>Restart in <span class="countdown"></span></h4> <h4>Restart in <span class="countdown"></span></h4>
% endif % endif

View File

@@ -96,8 +96,8 @@ DOCUMENTATION :: END
% if data['media_type'] != 'track': % if data['media_type'] != 'track':
<tr> <tr>
<td>Resolution</td> <td>Resolution</td>
<td>${VIDEO_RESOLUTION_OVERRIDES.get(data['stream_video_resolution'], data['stream_video_resolution'])}</td> <td>${data['stream_video_full_resolution']}</td>
<td>${VIDEO_RESOLUTION_OVERRIDES.get(data['video_resolution'], data['video_resolution'])}</td> <td>${data['video_full_resolution']}</td>
</tr> </tr>
% endif % endif
<tr> <tr>
@@ -178,6 +178,11 @@ DOCUMENTATION :: END
<td>${data['stream_video_framerate']}</td> <td>${data['stream_video_framerate']}</td>
<td>${data['video_framerate']}</td> <td>${data['video_framerate']}</td>
</tr> </tr>
<tr>
<td>Dynamic Range</td>
<td>${data['stream_video_dynamic_range']}</td>
<td>${data['video_dynamic_range']}</td>
</tr>
<tr> <tr>
<td>Aspect Ratio</td> <td>Aspect Ratio</td>
<td>-</td> <td>-</td>
@@ -230,7 +235,7 @@ DOCUMENTATION :: END
<tbody> <tbody>
<tr> <tr>
<td>Codec</td> <td>Codec</td>
<td>${data['stream_subtitle_codec'].upper()}</td> <td>${data['stream_subtitle_codec'].upper() or '-'}</td>
<td>${data['subtitle_codec'].upper()}</td> <td>${data['subtitle_codec'].upper()}</td>
</tr> </tr>
</tbody> </tbody>

View File

@@ -39,30 +39,43 @@ DOCUMENTATION :: END
<ul class="list-unstyled breadcrumb"> <ul class="list-unstyled breadcrumb">
% if query['media_type'] == 'movie': % if query['media_type'] == 'movie':
<li>Movies</li> <li>Movies</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['title']}</li> <li class="active">${query['title']}</li>
% elif query['media_type'] == 'show': % elif query['media_type'] == 'show':
<li>TV Shows</li> <li>TV Shows</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['grandparent_title']}</li> <li class="active">${query['grandparent_title']}</li>
% elif query['media_type'] == 'season': % elif query['media_type'] == 'season':
<li class="hidden-xs hidden-sm">TV Shows</li> <li class="hidden-xs hidden-sm">TV Shows</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li> <li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">Season ${query['parent_media_index']}</li> <li class="active">Season ${query['parent_media_index']}</li>
% elif query['media_type'] == 'episode': % elif query['media_type'] == 'episode':
<li class="hidden-xs hidden-sm">TV Shows</li> <li class="hidden-xs hidden-sm">TV Shows</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li> <li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li>Season ${query['parent_media_index']}</li> <li>Season ${query['parent_media_index']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">Episode ${query['media_index']} - ${query['title']}</li> <li class="active">Episode ${query['media_index']} - ${query['title']}</li>
% elif query['media_type'] == 'artist': % elif query['media_type'] == 'artist':
<li><Music</li> <li>Music</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['grandparent_title']}</li> <li class="active">${query['grandparent_title']}</li>
% elif query['media_type'] == 'album': % elif query['media_type'] == 'album':
<li class="hidden-xs hidden-sm">Music</li> <li class="hidden-xs hidden-sm">Music</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li>${query['grandparent_title']}</li> <li>${query['grandparent_title']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['parent_title']}</li> <li class="active">${query['parent_title']}</li>
% elif query['media_type'] == 'track': % elif query['media_type'] == 'track':
<li class="hidden-xs hidden-sm">Music</li> <li class="hidden-xs hidden-sm">Music</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="hidden-xs hidden-sm">${query['grandparent_title']}</li> <li class="hidden-xs hidden-sm">${query['grandparent_title']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li>${query['parent_title']}</li> <li>${query['parent_title']}</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">Track ${query['media_index']} - ${query['title']}</li> <li class="active">Track ${query['media_index']} - ${query['title']}</li>
% endif % endif
</ul> </ul>
@@ -127,6 +140,7 @@ DOCUMENTATION :: END
</%def> </%def>
<%def name="modalIncludes()"> <%def name="modalIncludes()">
% if query:
<div class="modal fade" id="confirm-modal-update" tabindex="-1" role="dialog" aria-labelledby="confirm-modal-update"> <div class="modal fade" id="confirm-modal-update" tabindex="-1" role="dialog" aria-labelledby="confirm-modal-update">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
@@ -157,6 +171,7 @@ DOCUMENTATION :: END
</p> </p>
<p> with </p> <p> with </p>
<p><span id="new_title"></span></p> <p><span id="new_title"></span></p>
<p>from the <strong><span id="new_library"></span></strong> library?</p>
% if query['media_type'] != 'movie': % if query['media_type'] != 'movie':
<p>All items for <strong>${query['grandparent_title']}</strong> will also be updated.</p> <p>All items for <strong>${query['grandparent_title']}</strong> will also be updated.</p>
% endif % endif
@@ -169,6 +184,7 @@ DOCUMENTATION :: END
</div> </div>
</div> </div>
</div> </div>
% endif
</%def> </%def>
<%def name="javascriptIncludes()"> <%def name="javascriptIncludes()">
@@ -183,6 +199,7 @@ DOCUMENTATION :: END
async: true, async: true,
data: { data: {
query: query_string, query: query_string,
limit: 30,
media_type: '${query["media_type"]}', media_type: '${query["media_type"]}',
season_index: '${query["parent_media_index"]}' season_index: '${query["parent_media_index"]}'
}, },
@@ -195,10 +212,12 @@ DOCUMENTATION :: END
$(document).on('click', '#search-results-list a', function (e) { $(document).on('click', '#search-results-list a', function (e) {
e.preventDefault(); e.preventDefault();
var new_rating_key = $(this).attr('id'); var new_rating_key = $(this).data('rating_key');
var new_library_section = $(this).data('library_name');
var new_href = $(this).attr('href'); var new_href = $(this).attr('href');
$('#new_title').html($(this).find('.item-children-instance-text-wrapper').html()); $('#new_title').html($(this).find('.item-children-instance-text-wrapper').html());
$('#new_library').text(new_library_section);
$('#confirm-modal-update').modal(); $('#confirm-modal-update').modal();
$('#confirm-modal-update').one('click', '#confirm-update', function () { $('#confirm-modal-update').one('click', '#confirm-update', function () {

View File

@@ -51,7 +51,13 @@ DOCUMENTATION :: END
<div class="col-md-12"> <div class="col-md-12">
<div class="table-card-back"> <div class="table-card-back">
<div class="user-info-wrapper"> <div class="user-info-wrapper">
<div class="user-info-poster-face" style="background-image: url(${data['user_thumb']});"></div> <div class="user-info-poster-face" style="background-image: url(${data['user_thumb']});">
% if not data['is_active']:
<span class="inactive-user-tooltip" data-toggle="tooltip" title="User not on Plex server">
<i class="fa fa-2x fa-exclamation-triangle"></i>
</span>
% endif
</div>
<div class="user-info-username"> <div class="user-info-username">
<span class="set-username">${data['friendly_name']}</span> <span class="set-username">${data['friendly_name']}</span>
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
@@ -168,6 +174,9 @@ DOCUMENTATION :: END
<label class="btn btn-dark"> <label class="btn btn-dark">
<input type="radio" name="media_type-filter" id="history-track" value="track" autocomplete="off"> Music <input type="radio" name="media_type-filter" id="history-track" value="track" autocomplete="off"> Music
</label> </label>
<label class="btn btn-dark">
<input type="radio" name="media_type-filter" id="history-live" value="live" autocomplete="off"> Live TV
</label>
</div> </div>
<div class="btn-group"> <div class="btn-group">
<button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button> <button class="btn btn-dark refresh-history-button" id="refresh-history-list"><i class="fa fa-refresh"></i> Refresh history</button>
@@ -184,6 +193,7 @@ DOCUMENTATION :: END
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="ip_address">IP Address</th> <th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Platform</th> <th align="left" id="platform">Platform</th>
<th align="left" id="product">Product</th>
<th align="left" id="player">Player</th> <th align="left" id="player">Player</th>
<th align="left" id="title">Title</th> <th align="left" id="title">Title</th>
<th align="left" id="started">Started</th> <th align="left" id="started">Started</th>
@@ -274,12 +284,13 @@ DOCUMENTATION :: END
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%"> <table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
<thead> <thead>
<tr> <tr>
<th align="left">Last Seen</th> <th align="left" id="last_seen">Last Streamed</th>
<th align="left">IP Address</th> <th align="left" id="first_seen">First Streamed</th>
<th align="left">Last Platform</th> <th align="left" id="ip_address">IP Address</th>
<th align="left">Last Player</th> <th align="left" id="platform">Last Platform</th>
<th align="left">Last Played</th> <th align="left" id="player">Last Player</th>
<th align="left">Play Count</th> <th align="left" id="last_played">Last Played</th>
<th align="left" id="play_count">Play Count</th>
</tr> </tr>
</thead> </thead>
</table> </table>
@@ -425,7 +436,7 @@ DOCUMENTATION :: END
history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options); history_table = $('#history_table-UID-${data["user_id"]}').DataTable(history_table_options);
history_table.column(2).visible(false); history_table.column(2).visible(false);
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, 12] });
$(colvis.button()).appendTo('#button-bar-history'); $(colvis.button()).appendTo('#button-bar-history');
clearSearchButton('history_table-UID-${data["user_id"]}', history_table); clearSearchButton('history_table-UID-${data["user_id"]}', history_table);
@@ -536,6 +547,8 @@ DOCUMENTATION :: END
login_log_table.draw(); login_log_table.draw();
}); });
$(".inactive-user-tooltip").tooltip();
% if _session['user_group'] == 'admin': % if _session['user_group'] == 'admin':
$("#edit-user-tooltip").tooltip(); $("#edit-user-tooltip").tooltip();
@@ -562,19 +575,17 @@ DOCUMENTATION :: END
$('#deleteType').text('history'); $('#deleteType').text('history');
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_history_rows',
url: 'delete_history_rows', type: 'POST',
type: 'POST', data: { row_ids: history_to_delete.join(',') },
data: { row_id: row }, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "History deleted";
var msg = "History deleted"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); history_table.draw();
} }
});
}); });
history_table.draw();
}); });
} }

View File

@@ -27,7 +27,7 @@ DOCUMENTATION :: END
<div id="user-player-image-${a['result_id']}"> <div id="user-player-image-${a['result_id']}">
<div class="user-player-instance-box svg-icon platform-${a['platform_name']}"></div> <div class="user-player-instance-box svg-icon platform-${a['platform_name']}"></div>
</div> </div>
<div class="user-player-instance-name"> <div class="user-player-instance-name" title="${a['player_name']}">
${a['player_name']} ${a['player_name']}
</div> </div>
<div class="user-player-instance-playcount"> <div class="user-player-instance-playcount">

View File

@@ -27,6 +27,9 @@ DOCUMENTATION :: END
</%doc> </%doc>
% if data: % if data:
<%
from plexpy.helpers import page
%>
<div class="dashboard-recent-media-row"> <div class="dashboard-recent-media-row">
<div id="recently-watched-row-scroller" style="left: 0;"> <div id="recently-watched-row-scroller" style="left: 0;">
<ul class="dashboard-recent-media list-unstyled"> <ul class="dashboard-recent-media list-unstyled">
@@ -35,12 +38,12 @@ DOCUMENTATION :: END
% if item['media_type'] == 'episode' or item['media_type'] == 'movie': % if item['media_type'] == 'episode' or item['media_type'] == 'movie':
% if item['rating_key']: % if item['rating_key']:
% if item['media_type'] == 'movie': % if item['media_type'] == 'movie':
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}"> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">
% elif item['media_type'] == 'episode': % elif item['media_type'] == 'episode':
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['grandparent_title']}"> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['grandparent_title']}">
% endif % endif
<div class="dashboard-recent-media-poster"> <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-poster-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 450, fallback='poster')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}"> <div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
<script> <script>
@@ -56,19 +59,38 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
% if item['media_type'] == 'episode': % if item['media_type'] == 'episode':
% if item['live']:
<h3> <h3>
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['grandparent_title']}">${item['grandparent_title']}</a> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
</h3> </h3>
<h3 class="text-muted" title="${item['title']}"> <h3 class="text-muted" title="${item['title']}">
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
</h3>
% if item['media_index']:
<h3 class="text-muted">
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
&middot; <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
</h3>
% else:
<h3 class="text-muted">
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['originally_available_at']}">${item['originally_available_at']}</a>
</h3>
% endif
% else:
<h3>
<a href="${page('info', item['grandparent_rating_key'])}" title="${item['grandparent_title']}">${item['grandparent_title']}</a>
</h3>
<h3 class="text-muted" title="${item['title']}">
<a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a href="info?rating_key=${item['parent_rating_key']}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a> <a href="${page('info', item['parent_rating_key'])}" title="Season ${item['parent_media_index']}">S${item['parent_media_index']}</a>
&middot; <a href="info?source=history&rating_key=${item['rating_key']}" title="Episode ${item['media_index']}">E${item['media_index']}</a> &middot; <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
</h3> </h3>
% endif
% elif item['media_type'] == 'movie': % elif item['media_type'] == 'movie':
<h3 title="${item['title']}"> <h3 title="${item['title']}">
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted">${item['year']}</h3> <h3 class="text-muted">${item['year']}</h3>
<h3 class="text-muted">&nbsp;</h3> <h3 class="text-muted">&nbsp;</h3>
@@ -94,9 +116,9 @@ DOCUMENTATION :: END
% endif % endif
% elif item['media_type'] == 'track': % elif item['media_type'] == 'track':
% if item['rating_key']: % if item['rating_key']:
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['parent_title']}"> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['parent_title']}">
<div class="dashboard-recent-media-cover"> <div class="dashboard-recent-media-cover">
<div class="dashboard-recent-media-cover-face" style="background-image: url(pms_image_proxy?img=${item['thumb']}&width=300&height=300&fallback=cover);"> <div class="dashboard-recent-media-cover-face" style="background-image: url(${page('pms_image_proxy', item['thumb'], item['rating_key'], 300, 300, fallback='cover')});">
<div class="dashboard-recent-media-overlay"> <div class="dashboard-recent-media-overlay">
<div class="dashboard-recent-media-overlay-text" id="time-${item['time']}"> <div class="dashboard-recent-media-overlay-text" id="time-${item['time']}">
<script> <script>
@@ -109,13 +131,13 @@ DOCUMENTATION :: END
</a> </a>
<div class="dashboard-recent-media-metacontainer"> <div class="dashboard-recent-media-metacontainer">
<h3 title="${item['original_title'] or item['grandparent_title']}"> <h3 title="${item['original_title'] or item['grandparent_title']}">
<a href="info?rating_key=${item['grandparent_rating_key']}" title="${item['original_title'] or item['grandparent_title']}">${item['original_title'] or item['grandparent_title']}</a> <a href="${page('info', item['grandparent_rating_key'])}" title="${item['original_title'] or item['grandparent_title']}">${item['original_title'] or item['grandparent_title']}</a>
</h3> </h3>
<h3 class="text-muted" title="${item['title']}"> <h3 class="text-muted" title="${item['title']}">
<a href="info?source=history&rating_key=${item['rating_key']}" title="${item['title']}">${item['title']}</a> <a href="${page('info', item['rating_key'], history=True, live=item['live'])}" title="${item['title']}">${item['title']}</a>
</h3> </h3>
<h3 class="text-muted"> <h3 class="text-muted">
<a href="info?rating_key=${item['parent_rating_key']}" title="${item['parent_title']}">${item['parent_title']}</a> <a href="${page('info', item['parent_rating_key'])}" title="${item['parent_title']}">${item['parent_title']}</a>
</h3> </h3>
</div> </div>
% else: % else:

View File

@@ -34,7 +34,7 @@
<th align="left" id="edit_row">Edit</th> <th align="left" id="edit_row">Edit</th>
<th align="right" id="avatar"></th> <th align="right" id="avatar"></th>
<th align="left" id="friendly_name">User</th> <th align="left" id="friendly_name">User</th>
<th align="left" id="last_seen">Last Seen</th> <th align="left" id="last_seen">Last Streamed</th>
<th align="left" id="last_known_ip">Last Known IP</th> <th align="left" id="last_known_ip">Last Known IP</th>
<th align="left" id="last_platform">Last Platform</th> <th align="left" id="last_platform">Last Platform</th>
<th align="left" id="last_player">Last Player</th> <th align="left" id="last_player">Last Player</th>
@@ -119,14 +119,14 @@
}); });
if (users_to_delete.length > 0) { if (users_to_delete.length > 0) {
$('#users-to-delete').prepend('<p>Are you REALLY sure you want to delete and purge all history for the following users:</p>') $('#users-to-delete').prepend('<p>Are you REALLY sure you want to delete and purge all history for the following users:</p>');
for (var i = 0; i < users_to_delete.length; i++) { for (var i = 0; i < users_to_delete.length; i++) {
$('#users-to-delete').append('<li>' + $('div[data-id=' + users_to_delete[i] + '] > input').val() + '</li>'); $('#users-to-delete').append('<li>' + $('div[data-id=' + users_to_delete[i] + '] > input').val() + '</li>');
} }
} }
if (users_to_purge.length > 0) { if (users_to_purge.length > 0) {
$('#users-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following users:</p>') $('#users-to-purge').prepend('<p>Are you REALLY sure you want to purge all history for the following users:</p>');
for (var i = 0; i < users_to_purge.length; i++) { for (var i = 0; i < users_to_purge.length; i++) {
$('#users-to-purge').append('<li>' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '</li>'); $('#users-to-purge').append('<li>' + $('div[data-id=' + users_to_purge[i] + '] > input').val() + '</li>');
} }
@@ -134,33 +134,30 @@
$('#confirm-modal-delete').modal(); $('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () { $('#confirm-modal-delete').one('click', '#confirm-delete', function () {
users_to_delete.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_all_user_history',
url: 'delete_user', type: 'POST',
type: 'POST', data: { row_ids: users_to_purge.join(',') },
data: { user_id: row }, cache: false,
cache: false, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "User history purged";
var msg = "User deleted"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); users_list_table.draw();
} }
});
}); });
users_to_purge.forEach(function(row, idx) { $.ajax({
$.ajax({ url: 'delete_user',
url: 'delete_all_user_history', type: 'POST',
type: 'POST', data: { row_ids: users_to_delete.join(',') },
data: { user_id: row }, cache: false,
cache: false, async: true,
async: true, success: function (data) {
success: function (data) { var msg = "User deleted";
var msg = "User history purged"; showMsg(msg, false, true, 2000);
showMsg(msg, false, true, 2000); users_list_table.draw();
} }
});
}); });
users_list_table.draw();
}); });
} }
@@ -192,7 +189,7 @@
complete: function (xhr, status) { complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText); var result = $.parseJSON(xhr.responseText);
var msg = result.message; var msg = result.message;
if (result.result == 'success') { if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false); showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
users_list_table.draw(); users_list_table.draw();
} else { } else {

View File

@@ -21,28 +21,21 @@
<link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet"> <link href="${http_root}css/font-awesome.v4-shims.min.css" rel="stylesheet">
<!-- Favicons --> <!-- 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="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.0"> <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.0"> <link rel="shortcut icon" href="${http_root}images/favicon/favicon.ico?v=2.0.5">
<!-- ICONS --> <!-- ICONS -->
<!-- Android >M39 icon --> <!-- Android -->
<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}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<link rel="manifest" href="${http_root}json/Android-manifest.json?v=2.0.0"> <meta name="theme-color" content="#282a2d">
<meta name="theme-color" content="#1f1f1f">
<!-- Apple --> <!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.0"> <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.0" color="#1f1f1f"> <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-title" content="Tautulli">
<meta name="apple-mobile-web-app-capable" content="yes"> <!-- Microsoft -->
<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 -->
<meta name="application-name" content="Tautulli"> <meta name="application-name" content="Tautulli">
<meta name="msapplication-TileColor" content="#1f1f1f"> <meta name="msapplication-config" content="${http_root}images/favicon/browserconfig.xml?v=2.0.5">
<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" />
</head> </head>
<body> <body>
@@ -67,8 +60,38 @@
</p> </p>
</div> </div>
</div> </div>
<div class="wizard-card" data-cardname="card2"> <div class="wizard-card" data-cardname="card2">
<h3>Plex Authentication</h3> <h3>Authentication</h3>
<div class="wizard-input-section">
<p class="help-block">
Please setup an admin username and password for Tautulli.
</p>
</div>
<div class="wizard-input-section">
<label for="http_username">HTTP Username</label>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control auth-settings" id="http_username" name="http_username" value="" size="30">
</div>
</div>
</div>
<div class="wizard-input-section">
<label for="http_password">HTTP Password</label>
<div class="row">
<div class="col-xs-8">
<input type="password" class="form-control auth-settings" id="http_password" name="http_password" value="" size="30" autocomplete="new-password">
</div>
</div>
</div>
<input type="hidden" class="form-control" name="http_hash_password" id="http_hash_password" value="1">
<input type="hidden" class="form-control" name="http_plex_admin" id="http_plex_admin" value="1">
<input type="hidden" id="authentication_valid" data-validate="validateAuthentication" value="">
<span style="display: none;" id="authentication-status"></span>
</div>
<div class="wizard-card" data-cardname="card3">
<h3>Plex Account</h3>
<div class="wizard-input-section"> <div class="wizard-input-section">
<p class="help-block"> <p class="help-block">
Tautulli requires a Plex.tv account. Click the button below to sign in on Plex.tv. You may need to allow popups in your browser. Tautulli requires a Plex.tv account. Click the button below to sign in on Plex.tv. You may need to allow popups in your browser.
@@ -78,7 +101,8 @@
<a class="btn btn-dark" id="sign-in-plex" href="#" role="button">Sign In with Plex</a> <a class="btn btn-dark" id="sign-in-plex" href="#" role="button">Sign In with Plex</a>
<span style="margin-left: 10px; display: none;" id="pms-token-status"></span> <span style="margin-left: 10px; display: none;" id="pms-token-status"></span>
</div> </div>
<div class="wizard-card" data-cardname="card3">
<div class="wizard-card" data-cardname="card4">
<h3>Plex Media Server</h3> <h3>Plex Media Server</h3>
<div class="wizard-input-section"> <div class="wizard-input-section">
<p class="help-block"> <p class="help-block">
@@ -137,7 +161,7 @@
<span style="margin-left: 10px; display: none;" id="pms-verify-status"></span> <span style="margin-left: 10px; display: none;" id="pms-verify-status"></span>
</div> </div>
<div class="wizard-card" data-cardname="card4"> <div class="wizard-card" data-cardname="card5">
<h3>Activity Logging</h3> <h3>Activity Logging</h3>
<div class="wizard-input-section"> <div class="wizard-input-section">
<p class="help-block"> <p class="help-block">
@@ -162,7 +186,7 @@
</div> </div>
</div> </div>
<div class="wizard-card" data-cardname="card4"> <div class="wizard-card" data-cardname="card6">
<h3>Notifications</h3> <h3>Notifications</h3>
<div class="wizard-input-section"> <div class="wizard-input-section">
<p class="help-block"> <p class="help-block">
@@ -175,11 +199,11 @@
</div> </div>
</div> </div>
<div class="wizard-card" data-cardname="card5"> <div class="wizard-card" data-cardname="card7">
<h3>Database Import</h3> <h3>Database Import</h3>
<div class="wizard-input-section"> <div class="wizard-input-section">
<p class="help-block"> <p class="help-block">
If you have an existing PlexWatch/Plexivity database, you can import the data into Tautulli. If you have an existing Tautulli, PlexWatch, or Plexivity database, you can import the data into Tautulli.
</p> </p>
<p class="help-block"> <p class="help-block">
To import a database, navigate to the <strong>Settings</strong> page To import a database, navigate to the <strong>Settings</strong> page
@@ -192,6 +216,8 @@
<input type="checkbox" name="first_run" id="first_run" value="1" checked> <input type="checkbox" name="first_run" id="first_run" value="1" checked>
<input type="checkbox" name="group_history_tables" id="group_history_tables" value="1" checked> <input type="checkbox" name="group_history_tables" id="group_history_tables" value="1" checked>
<input type="checkbox" name="history_table_activity" id="history_table_activity" value="1" checked> <input type="checkbox" name="history_table_activity" id="history_table_activity" value="1" checked>
<input type="checkbox" name="sys_tray_icon" id="sys_tray_icon" value="1" checked>
<input type="checkbox" name="launch_startup" id="launch_startup" value="1" checked>
<input type="checkbox" name="launch_browser" id="launch_browser" value="1" checked> <input type="checkbox" name="launch_browser" id="launch_browser" value="1" checked>
<input type="checkbox" name="api_enabled" id="api_enabled" value="1" checked> <input type="checkbox" name="api_enabled" id="api_enabled" value="1" checked>
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" checked> <input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" checked>
@@ -199,6 +225,8 @@
<input type="checkbox" name="check_github" id="check_github" value="1" checked> <input type="checkbox" name="check_github" id="check_github" value="1" checked>
<input type="checkbox" name="log_blacklist" id="log_blacklist" value="1" checked> <input type="checkbox" name="log_blacklist" id="log_blacklist" value="1" checked>
<input type="checkbox" name="cache_images" id="cache_images" value="1" checked> <input type="checkbox" name="cache_images" id="cache_images" value="1" checked>
<input type="checkbox" name="notify_group_recently_added_grandparent" id="notify_group_recently_added_grandparent" value="1" checked>
<input type="checkbox" name="notify_group_recently_added_parent" id="notify_group_recently_added_parent" value="1" checked>
<input type="checkbox" name="server_changed" id="server_changed" value="1" checked> <input type="checkbox" name="server_changed" id="server_changed" value="1" checked>
<input type="checkbox" name="first_run_complete" id="first_run_complete" value="1" checked> <input type="checkbox" name="first_run_complete" id="first_run_complete" value="1" checked>
<input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard"> <input type="text" name="home_stats_cards" id="home_stats_cards" value="first_run_wizard">
@@ -227,11 +255,29 @@
<script src="${http_root}js/script.js${cache_param}"></script> <script src="${http_root}js/script.js${cache_param}"></script>
<script src="${http_root}js/bootstrap-wizard.min.js"></script> <script src="${http_root}js/bootstrap-wizard.min.js"></script>
<script> <script>
function validateAuthentication(el) {
var http_username = $("#http_username").val();
var http_password = $("#http_password").val();
var valid_authentication = el.val();
var retValue = {};
if (http_username === "" || http_password === "") {
retValue.status = false;
retValue.msg = "Please enter a username and password.";
$("#authentication-status").html('<i class="fa fa-exclamation-circle"></i> Please enter a username and password.');
$('#authentication-status').fadeIn('fast').delay(2000).fadeOut('fast');
} else {
retValue.status = true;
}
return retValue;
}
function validatePMSip(el) { function validatePMSip(el) {
var valid_pms_ip = el.val(); var valid_pms_ip = el.val();
var retValue = {}; var retValue = {};
if (valid_pms_ip == "") { if (valid_pms_ip === "") {
retValue.status = false; retValue.status = false;
retValue.msg = "Please verify your server."; retValue.msg = "Please verify your server.";
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please verify your server.'); $("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i> Please verify your server.');
@@ -247,7 +293,7 @@
var valid_pms_token = el.val(); var valid_pms_token = el.val();
var retValue = {}; var retValue = {};
if (valid_pms_token == "") { if (valid_pms_token === "") {
retValue.status = false; retValue.status = false;
retValue.msg = "Please authenticate."; retValue.msg = "Please authenticate.";
$("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Please authenticate.'); $("#pms-token-status").html('<i class="fa fa-exclamation-circle"></i> Please authenticate.');
@@ -284,7 +330,7 @@ $(document).ready(function() {
$.fn.wizard.logging = false; $.fn.wizard.logging = false;
var options = { var options = {
keyboard : false, keyboard : false,
contentHeight : 400, contentHeight : 450,
contentWidth : 700, contentWidth : 700,
backdrop: 'static', backdrop: 'static',
buttons: {submitText: 'Finish'}, buttons: {submitText: 'Finish'},
@@ -449,7 +495,7 @@ $(document).ready(function() {
var pms_ssl = $("#pms_ssl").val(); var pms_ssl = $("#pms_ssl").val();
var pms_is_remote = $("#pms_is_remote").val(); var pms_is_remote = $("#pms_is_remote").val();
if ((pms_ip !== '') || (pms_port !== '')) { if ((pms_ip !== '') || (pms_port !== '')) {
$("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Validating server...'); $("#pms-verify-status").html('<i class="fa fa-refresh fa-spin"></i>&nbsp; Verifying server...');
$('#pms-verify-status').fadeIn('fast'); $('#pms-verify-status').fadeIn('fast');
$.ajax({ $.ajax({
url: 'get_server_id', url: 'get_server_id',
@@ -464,7 +510,7 @@ $(document).ready(function() {
async: true, async: true,
timeout: 5000, timeout: 5000,
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown) {
$("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; This is not a Plex Server!'); $("#pms-verify-status").html('<i class="fa fa-exclamation-circle"></i>&nbsp; Error verifying server: ' + textStatus);
$('#pms-verify-status').fadeIn('fast'); $('#pms-verify-status').fadeIn('fast');
}, },
success: function(xhr, status) { success: function(xhr, status) {

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