Compare commits

...

586 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
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
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
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
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
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
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
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
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
754 changed files with 121805 additions and 33182 deletions

View File

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

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

View File

@@ -1,30 +0,0 @@
name: Publish Docker Branch
on:
push:
branches: [master, beta, nightly]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Branch
run: echo ::set-env name=BRANCH::${GITHUB_REF#refs/heads/}
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
env:
VERSION: ${{ github.sha }}
with:
name: tautulli/tautulli
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
buildargs: VERSION, BRANCH
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
job: ${{ github.workflow }}
nofail: true

View File

@@ -1,34 +0,0 @@
name: Publish Docker Release
on:
push:
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Branch
run: echo ::set-env name=BRANCH::${GITHUB_REF#refs/heads/}
- name: Get Release Version
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF/refs\/tags\//}
- name: Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
env:
VERSION: ${{ github.sha }}
with:
name: tautulli/tautulli
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
buildargs: VERSION, BRANCH
tags: ${{ env.RELEASE_VERSION }}
- name: Post Status to Discord
uses: sarisia/actions-status-discord@v1
if: always()
with:
webhook: ${{ secrets.DISCORD_WEBHOOK }}
status: ${{ job.status }}
job: ${{ github.workflow }}
nofail: true

View File

@@ -1,29 +0,0 @@
name: Create Pre-Release
on:
push:
tags:
- 'v*-beta'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Release Version
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF/refs\/tags\//}
- name: Get Changelog
run: echo ::set-env 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: ${{ env.RELEASE_VERSION }}
release_name: Tautulli ${{ env.RELEASE_VERSION }}
body: |
## Changelog
##${{ env.CHANGELOG }}
draft: false
prerelease: true

View File

@@ -1,30 +0,0 @@
name: Create Release
on:
push:
tags:
- 'v*'
- '!v*-beta'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@master
- name: Get Release Version
run: echo ::set-env name=RELEASE_VERSION::${GITHUB_REF/refs\/tags\//}
- name: Get Changelog
run: echo ::set-env 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: ${{ env.RELEASE_VERSION }}
release_name: Tautulli ${{ env.RELEASE_VERSION }}
body: |
## Changelog
##${{ env.CHANGELOG }}
draft: false
prerelease: false

6
.gitignore vendored
View File

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

175
API.md
View File

@@ -85,10 +85,11 @@ Delete all Tautulli history for a specific library.
```
Required parameters:
server_id (str): The Plex server identifier of the library section
section_id (str): The id of the Plex library section
Optional parameters:
None
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns:
None
@@ -103,7 +104,7 @@ Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
None
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns:
None
@@ -114,6 +115,21 @@ Returns:
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 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:
server_id (str): The Plex server identifier of the library section
section_id (str): The id of the Plex library section
Optional parameters:
None
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns:
None
@@ -173,10 +190,13 @@ Delete the 3rd party API lookup info.
```
Required parameters:
None
Optional parameters:
rating_key (int): 1234
(Note: Must be the movie, show, artist, album, or track rating key)
Optional parameters:
None
service (str): 'themoviedb' or 'tvmaze' or 'musicbrainz'
delete_all (bool): 'true' to delete all images form the service
Returns:
json:
@@ -275,6 +295,10 @@ Returns:
```
### delete_recently_added
Flush out all of the recently added items in the database.
### delete_temp_sessions
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
Optional parameters:
None
row_ids (str): Comma separated row ids to delete, e.g. "2,3,8"
Returns:
None
@@ -327,6 +351,7 @@ Required parameters:
Optional parameters:
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
Returns:
@@ -402,6 +427,7 @@ Returns:
"children_count": "",
"collections": [],
"container": "mkv",
"container_decision": "direct play",
"content_rating": "TV-MA",
"deleted_user": 0,
"device": "Windows",
@@ -647,7 +673,7 @@ Returns:
### 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:
@@ -658,7 +684,7 @@ Optional parameters:
Returns:
json:
{"continent": "North America",
{"code": 'US",
"country": "United States",
"region": "California",
"city": "Mountain View",
@@ -668,9 +694,6 @@ Returns:
"longitude": -122.0838,
"accuracy": 1000
}
json:
{"error": "The address 127.0.0.1 is not in the database."
}
```
@@ -718,7 +741,6 @@ Returns:
"group_count": 1,
"group_ids": "1124",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1124,
"ip_address": "xxx.xxx.xxx.xxx",
"live": 0,
"media_index": 17,
@@ -734,6 +756,7 @@ Returns:
"player": "Castle-PC",
"rating_key": 4348,
"reference_id": 1123,
"row_id": 1124,
"session_key": null,
"started": 1462688107,
"state": null,
@@ -845,6 +868,7 @@ Returns:
[{"art": "/:/resources/show-fanart.jpg",
"child_count": "3745",
"count": "62",
"is_active": 1,
"parent_count": "240",
"section_id": "2",
"section_name": "TV Shows",
@@ -886,7 +910,8 @@ Returns:
"do_notify_created": "Checked",
"duration": 1578037,
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1128,
"histroy_row_id": 1128,
"is_active": 1,
"keep_history": "Checked",
"labels": [],
"last_accessed": 1462693216,
@@ -902,9 +927,11 @@ Returns:
"parent_title": "",
"plays": 772,
"rating_key": 153037,
"row_id": 1,
"section_id": 2,
"section_name": "TV Shows",
"section_type": "Show",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w",
"thumb": "/library/metadata/153036/thumb/1462175062",
"year": 2016
},
@@ -932,13 +959,16 @@ Returns:
"deleted_section": 0,
"do_notify": 1,
"do_notify_created": 1,
"is_active": 1,
"keep_history": 1,
"library_art": "/:/resources/movie-fanart.jpg",
"library_thumb": "/:/resources/movie.png",
"parent_count": null,
"row_id": 1,
"section_id": 1,
"section_name": "Movies",
"section_type": "movie"
"section_type": "movie",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
}
```
@@ -1036,12 +1066,14 @@ Returns:
[{"friendly_name": "Jon Snow",
"total_plays": 170,
"user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar"
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
"username": "LordCommanderSnow"
},
{"platform_type": "DanyKhaleesi69",
"total_plays": 42,
"user_id": 8008135,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar"
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar",
"username: "DanyKhaleesi69"
},
{...},
{...}
@@ -1054,10 +1086,11 @@ Get a library's watch time statistics.
```
Required parameters:
section_id (str): The id of the Plex library section
section_id (str): The id of the Plex library section
Optional parameters:
grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns:
json:
@@ -1150,6 +1183,7 @@ Returns:
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"guids": [],
"labels": [],
"last_viewed_at": "1462165717",
"library_name": "TV Shows",
@@ -1871,6 +1905,7 @@ Returns:
"grandparent_thumb": "/library/metadata/1219/thumb/1462175063",
"grandparent_title": "Game of Thrones",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"guids": [],
"labels": [],
"last_viewed_at": "1462165717",
"library_name": "TV Shows",
@@ -1959,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 all your servers that are published to Plex.tv.
@@ -2214,10 +2276,13 @@ Returns:
"do_notify": 1,
"email": "Jon.Snow.1337@CastleBlack.com",
"friendly_name": "Jon Snow",
"is_active": 1,
"is_admin": 0,
"is_allow_sync": 1,
"is_home_user": 1,
"is_restricted": 0,
"keep_history": 1,
"row_id": 1,
"shared_libraries": ["10", "1", "4", "5", "15", "20", "2"],
"user_id": 133788,
"user_thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
@@ -2234,8 +2299,8 @@ Required parameters:
user_id (str): The id of the Plex user
Optional parameters:
order_column (str): "last_seen", "ip_address", "platform", "player",
"last_played", "play_count"
order_column (str): "last_seen", "first_seen", "ip_address", "platform",
"player", "last_played", "play_count"
order_dir (str): "desc" or "asc"
start (int): Row to start from, 0
length (int): Number of items to return, 25
@@ -2253,6 +2318,7 @@ Returns:
"ip_address": "xxx.xxx.xxx.xxx",
"last_played": "Game of Thrones - The Red Woman",
"last_seen": 1462591869,
"first_seen": 1583968210,
"live": 0,
"media_index": 1,
"media_type": "episode",
@@ -2370,6 +2436,7 @@ Required parameters:
Optional parameters:
grouping (int): 0 or 1
query_days (str): Comma separated days, e.g. "1,7,30,0"
Returns:
json:
@@ -2413,11 +2480,13 @@ Returns:
"filter_music": "",
"filter_photos": "",
"filter_tv": "",
"is_active": 1,
"is_admin": 0,
"is_allow_sync": 1,
"is_home_user": 1,
"is_restricted": 0,
"keep_history": 1,
"row_id": 1,
"server_token": "PU9cMuQZxJKFBtGqHk68",
"shared_libraries": "1;2;3",
"thumb": "https://plex.tv/users/k10w42309cynaopq/avatar",
@@ -2457,8 +2526,9 @@ Returns:
"duration": 2998290,
"friendly_name": "Jon Snow",
"guid": "com.plexapp.agents.thetvdb://121361/6/1?lang=en",
"id": 1121,
"history_row_id": 1121,
"ip_address": "xxx.xxx.xxx.xxx",
"is_active": 1,
"keep_history": "Checked",
"last_played": "Game of Thrones - The Red Woman",
"last_seen": 1462591869,
@@ -2472,10 +2542,12 @@ Returns:
"player": "Plex Web (Chrome)",
"plays": 487,
"rating_key": 153037,
"row_id": 1,
"thumb": "/library/metadata/153036/thumb/1462175062",
"transcode_decision": "transcode",
"user_id": 133788,
"user_thumb": "https://plex.tv/users/568gwwoib5t98a3a/avatar",
"username": "LordCommanderSnow",
"year": 2016
},
{...},
@@ -2516,25 +2588,54 @@ Returns:
```
### import_database
Import a PlexWatch or Plexivity database into Tautulli.
### import_config
Import a Tautulli config file.
```
Required parameters:
app (str): "plexwatch" or "plexivity"
database_path (str): The full path to the plexwatch database file
table_name (str): "processed" or "grouped"
config_file (file): The config file to import (multipart/form-data)
or
config_path (str): The full path to the config file to import
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:
None
json:
{"result": "success",
"message": "Config import has started. Check the logs to monitor any problems. "
"Tautulli will restart automatically."
}
```
### install_geoip_db
Downloads and installs the GeoLite2 database
### import_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
@@ -2629,14 +2730,18 @@ Registers the Tautulli Android App for notifications.
```
Required parameters:
device_name (str): The device name of the Tautulli Android App
device_id (str): The OneSignal device id of the Tautulli Android App
device_id (str): The unique device identifier for the mobile device
device_name (str): The device name of the mobile device
Optional parameters:
friendly_name (str): A friendly name to identify the mobile device
onesignal_id (str): The OneSignal id for the mobile device
Returns:
None
json:
{"pms_name": "Winterfell-Server",
"server_id": "ds48g4r354a8v9byrrtr697g3g79w"
}
```
@@ -2733,7 +2838,7 @@ Returns:
### sql
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
manually enabled in the config file.
manually enabled in the config file while Tautulli is shut down.
```
Required parameters:
@@ -2813,10 +2918,6 @@ Returns:
```
### uninstall_geoip_db
Uninstalls the GeoLite2 database
### update
Update Tautulli.

View File

@@ -1,10 +1,198 @@
# Changelog
## v2.2.0-beta (2020-02-27)
## 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:
@@ -18,8 +206,17 @@
* 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.

View File

@@ -9,7 +9,7 @@ All pull requests should be based on the `nightly` branch, to minimize cross mer
### Python Code
#### 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.
@@ -38,4 +38,4 @@ HTML5 compatible browsers are targeted.
* 4 space indentation
* `methodName`
* `variableName`
* `ClassName`
* `ClassName`

View File

@@ -1,9 +1,9 @@
FROM python:2.7.17-slim
FROM tautulli/tautulli-baseimage:python3
LABEL maintainer="TheMeanCanEHdian"
LABEL maintainer="Tautulli"
ARG VERSION
ARG BRANCH
ARG COMMIT
ENV TAUTULLI_DOCKER=True
ENV TZ=UTC
@@ -11,21 +11,13 @@ ENV TZ=UTC
WORKDIR /app
RUN \
apt-get -q -y update --no-install-recommends && \
apt-get install -q -y --no-install-recommends \
curl && \
rm -rf /var/lib/apt/lists/* && \
pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir --upgrade \
pycryptodomex \
pyopenssl && \
echo ${VERSION} > /app/version.txt && \
echo ${BRANCH} > /app/branch.txt
echo ${BRANCH} > /app/branch.txt && \
echo ${COMMIT} > /app/version.txt
COPY . /app
CMD [ "python", "Tautulli.py", "--datadir", "/config" ]
ENTRYPOINT [ "./start.sh" ]
VOLUME /config /plex_logs
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,8 +1,4 @@
#!/bin/sh
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
''''which python2.7 >/dev/null 2>&1 && exec python2.7 "$0" "$@" # '''
''''exec echo "Error: Python not found!" # '''
#!/usr/bin/env python
# -*- coding: utf-8 -*-

View File

@@ -29,14 +29,15 @@ This project is based on code from [Headphones](https://github.com/rembo10/headp
## Installation & Support
[![Python](https://img.shields.io/badge/python-v2.7.17-blue?style=flat-square)](https://python.org/downloads/release/python-2717/)
[![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@nightly](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/tautulli-tautulli: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%20Branch/master?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Amaster) | [![Docker@beta](https://img.shields.io/badge/tautulli-tautulli: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%20Branch/beta?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Abeta) | [![Docker@nightly](https://img.shields.io/badge/tautulli-tautulli: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%20Branch/nightly?style=flat-square)](https://github.com/Tautulli/Tautulli/actions?query=branch%3Anightly) |
| 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)

View File

@@ -1,8 +1,4 @@
#!/bin/sh
''''which python >/dev/null 2>&1 && exec python "$0" "$@" # '''
''''which python2 >/dev/null 2>&1 && exec python2 "$0" "$@" # '''
''''which python2.7 >/dev/null 2>&1 && exec python2.7 "$0" "$@" # '''
''''exec echo "Error: Python not found!" # '''
#!/usr/bin/env python
# -*- coding: utf-8 -*-
@@ -27,17 +23,24 @@ import sys
# Ensure lib added to path, before any other imports
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 datetime
import locale
import pytz
import signal
import time
import threading
import tzlocal
import plexpy
from plexpy import config, database, helpers, 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
signal.signal(signal.SIGINT, plexpy.sig_handler)
@@ -51,12 +54,14 @@ def main():
"""
# 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.PROG_DIR = sys._MEIPASS
else:
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:]
# From sickbeard
@@ -122,12 +127,11 @@ def main():
if args.dev:
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 sys.platform == 'win32':
sys.stderr.write(
"Daemonizing not supported under Windows, starting normally\n")
logger.warn("Daemonizing not supported under Windows, starting normally")
else:
plexpy.DAEMON = True
plexpy.QUIET = True
@@ -145,11 +149,13 @@ def main():
try:
with open(plexpy.PIDFILE, 'r') as fp:
pid = int(fp.read())
os.kill(pid, 0)
except IOError as e:
raise SystemExit("Unable to read PID file: %s", e)
try:
os.kill(pid, 0)
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." %
(plexpy.PIDFILE, pid))
else:
@@ -175,6 +181,8 @@ def main():
# Determine which data directory and config file to use
if args.datadir:
plexpy.DATA_DIR = args.datadir
elif plexpy.FROZEN:
plexpy.DATA_DIR = appdirs.user_data_dir("Tautulli", False)
else:
plexpy.DATA_DIR = plexpy.PROG_DIR
@@ -229,25 +237,51 @@ def main():
try:
import OpenSSL
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.")
plexpy.CONFIG.ENABLE_HTTPS = False
# Try to start the server. Will exit here is address is already in use.
webstart.start()
# Windows system tray icon
if os.name == 'nt' and plexpy.CONFIG.WIN_SYS_TRAY:
plexpy.win_system_tray()
logger.info("Tautulli is ready!")
if common.PLATFORM == 'Windows':
if plexpy.CONFIG.SYS_TRAY_ICON:
plexpy.WIN_SYS_TRAY_ICON = windows.WindowsSystemTray()
plexpy.WIN_SYS_TRAY_ICON.start()
windows.set_startup()
elif common.PLATFORM == 'Darwin':
macos.set_startup()
# Open webbrowser
if plexpy.CONFIG.LAUNCH_BROWSER and not args.nolaunch and not plexpy.DEV:
plexpy.launch_browser(plexpy.CONFIG.HTTP_HOST, plexpy.HTTP_PORT,
plexpy.HTTP_ROOT)
# Wait endlessy for a signal to happen
if common.PLATFORM == 'Darwin' and plexpy.CONFIG.SYS_TRAY_ICON:
if not macos.HAS_PYOBJC:
logger.warn("The pyobjc module is missing. Install this "
"module to enable the MacOS menu bar icon.")
plexpy.CONFIG.SYS_TRAY_ICON = False
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:
if not plexpy.SIGNAL:
try:
@@ -263,11 +297,16 @@ def main():
plexpy.shutdown(restart=True)
elif plexpy.SIGNAL == 'checkout':
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)
else:
logger.error('Unknown signal. Shutting down...')
plexpy.shutdown()
plexpy.SIGNAL = None
# Call main()
if __name__ == "__main__":
main()

View File

@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
# 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."

View File

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

View File

@@ -5,46 +5,105 @@
<h4 class="modal-title">Import ${app} Database</h4>
</div>
<div class="modal-body" id="modal-text">
<p class="help-block">
<%
v = ''
if app == 'PlexWatch':
v = '0.3.2'
elif app == 'Plexivity':
v = '0.9.8'
%>
<strong>Please ensure your ${app} database is at version ${v} or higher.</strong>
</p>
<div class="form-group">
<label for="db_location">Database Location</label>
<div class="row">
<div class="col-xs-8">
<input type="text" class="form-control" id="db_location" name="db_location" value="" required>
<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()}" />
% if app in ('PlexWatch', 'Plexivity'):
<p class="help-block">
<%
v = ''
if app == 'PlexWatch':
v = '0.3.2'
elif app == 'Plexivity':
v = '0.9.8'
%>
<strong>Please ensure your ${app} database is at version ${v} or higher.</strong>
</p>
% endif
<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>
<p class="help-block">Upload the ${app} database file you wish to import.</p>
</div>
<p class="help-block">Enter the path and file name for the ${app} database you wish to import.</p>
</div>
<div class="form-group">
<label for="table_name">Table Name</label>
<div class="row">
<div class="col-xs-4">
<select id="table_name" class="form-control" name="table_name">
<option value="processed">processed</option>
<option value="grouped">grouped</option>
</select>
<div class="form-group">
<label for="import_database_path">Option 2: Browse for a Database 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_database_path_browse" data-toggle="browse" data-description="Database File" data-filter=".db" data-target="#import_database_path">Browse</button>
</span>
<input type="text" class="form-control" id="import_database_path" name="import_database_path" value="" placeholder="tautulli.db" required disabled>
</div>
</div>
</div>
<p class="help-block">Browse for the ${app} database file you wish to import.</p>
</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>
</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>
% if app == 'Tautulli':
<div class="form-group">
<label for="table_name">Import Method</label>
<div class="row">
<div class="col-xs-4">
<select class="form-control" id="import_method" name="import_method">
<option value="merge">Merge</option>
<option value="overwrite">Overwrite</option>
</select>
</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>
<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>
<div class="checkbox">
<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 class="modal-footer">
<div>
@@ -55,24 +114,87 @@
</div>
</div>
<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() {
var database_path = $("#db_location").val();
var table_name = $("#table_name").val();
var import_ignore_interval = $("#import_ignore_interval").val();
$(this).prop('disabled', true);
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({
url: 'import_database',
data: {
app: "${app}",
database_path: database_path,
table_name: table_name,
import_ignore_interval: import_ignore_interval
},
type: 'POST',
data: data,
cache: false,
async: true,
contentType: content_type,
processData: process_data,
success: function(data) {
$("#status-message").html(data);
$("#db_location").val('')
var msg;
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

@@ -55,8 +55,10 @@
newer version</a> of Tautulli is available!<br />
You are ${plexpy.COMMITS_BEHIND} commit${'s' if plexpy.COMMITS_BEHIND > 1 else ''} behind.<br />
% endif
% if plexpy.DOCKER:
% 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>
% endif
@@ -228,20 +230,12 @@ ${next.modalIncludes()}
</div>
</div>
<ul id="donation_type" class="nav nav-pills" role="tablist" style="display: flex; justify-content: center; margin: 10px 0;">
<li class="active"><a href="#patreon-donation" role="tab" data-toggle="tab">Patreon</a></li>
<li><a href="#github-donation" role="tab" data-toggle="tab">GitHub</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>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="patreon-donation" style="text-align: center">
<p>
Click the button below to continue to Patreon.
</p>
<a href="${anon_url('https://www.patreon.com/join/tautulli')}" target="_blank">
<img src="images/become_a_patron_button.png" alt="Become a Patron" height="40">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="github-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>
@@ -249,6 +243,14 @@ ${next.modalIncludes()}
<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>
Click the button below to continue to Patreon.
</p>
<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">
</a>
</div>
<div role="tabpanel" class="tab-pane" id="paypal-donation" style="text-align: center">
<p>
Click the button below to continue to PayPal.
@@ -291,11 +293,10 @@ ${next.modalIncludes()}
<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/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/jquery.tripleclick.min.js"></script>
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
<script src="${http_root}js/ajaxNotifications.js"></script>
% endif
<script>
% if _session['user_group'] == 'admin':
$('body').on('click', '#updateDismiss', function() {
@@ -329,8 +330,10 @@ ${next.modalIncludes()}
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.docker) {
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>';
}
@@ -418,6 +421,10 @@ ${next.modalIncludes()}
$(document).on('hidden.bs.modal', '.modal', function () {
$('.modal:visible').length && $(document.body).addClass('modal-open');
});
% if _session['user_group'] == 'admin' and BROWSER_NOTIFIERS:
check_notifications();
% endif
});
% 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

@@ -1,6 +1,6 @@
body {
font-family: 'Open Sans', Arial, sans-serif;
color: #fff;
color: #eee;
margin-top: 50px;
overflow: hidden;
}
@@ -36,7 +36,7 @@ select.input-sm {
select[multiple] {
height: 125px;
margin: 5px 0 5px 0;
color: #fff;
color: #eee;
border: 0px solid #444;
background: #555;
padding: 2px 2px;
@@ -48,7 +48,7 @@ select[multiple]:focus {
outline: 0;
outline: thin dotted \9;
color: #555;
background-color: #fff;
background-color: #eee;
transition: background-color .3s;
}
select[multiple]:focus::-webkit-scrollbar-thumb {
@@ -63,7 +63,7 @@ select[multiple] option {
select.form-control,
div.form-control .selectize-input {
margin: 5px 0 5px 0;
color: #fff;
color: #eee;
border: 0px solid #444;
background: #555;
padding: 6px 12px;
@@ -76,7 +76,7 @@ select.form-control {
}
.react-selectize.root-node .react-selectize-control,
.selectize-control.form-control .selectize-input {
color: #fff !important;
color: #eee !important;
border: 0px solid #444 !important;
background: #555 !important;
padding: 1px 2px;
@@ -123,15 +123,15 @@ select.form-control {
cursor: pointer;
}
.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 {
fill: #fff !important;
fill: #eee !important;
}
.react-selectize.root-node .simple-value,
.selectize-control.multi .selectize-input > div {
background: #444444 !important;
color: #ffffff !important;
background: #444 !important;
color: #eee !important;
padding-bottom: 2px !important;
transition: background-color .3s;
}
@@ -156,7 +156,7 @@ select.form-control:focus,
outline: 0;
outline: thin dotted \9;
color: #555 !important;
background-color: #fff !important;
background-color: #eee !important;
transition: background-color .3s;
}
.react-selectize.root-node.open .simple-value,
@@ -219,7 +219,7 @@ select.form-control:focus,
}
select.form-control option {
color: #555;
background-color: #fff;
background-color: #eee;
}
img {
-webkit-box-sizing: content-box;
@@ -278,13 +278,13 @@ object {
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
color: #fff;
color: #eee;
background-color: #2f2f2f;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
color: #fff;
color: #eee;
background-color: #2f2f2f;
}
.dropdown-menu > .disabled > a,
@@ -327,14 +327,14 @@ object {
background-color: #3B3B3B;
}
.btn-dark:hover {
color: #fff;
color: #eee;
background-color: #333;
border-color: #444;
}
.btn-dark:active,
.btn-dark.active,
.open > .dropdown-toggle.btn-dark {
color: #fff;
color: #eee;
background-color: #333;
border-color: #444;
}
@@ -347,7 +347,7 @@ object {
.btn-dark:active.focus,
.btn-dark.active.focus,
.open > .dropdown-toggle.btn-dark.focus {
color: #fff;
color: #eee;
background-color: #333;
}
.btn-dark:active,
@@ -387,24 +387,24 @@ fieldset[disabled] .btn-dark.active {
background-color: #3B3B3B;
}
.btn-bright {
color: #fff;
color: #eee;
background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b;
}
.btn-bright:focus,
.btn-bright.focus {
color: #fff;
color: #eee;
background-color: #eb8600;
}
.btn-bright:hover {
color: #fff;
color: #eee;
background-color: #e59029;
box-shadow: inset 0 1px 0 #ebac60;
}
.btn-bright:active,
.btn-bright.active,
.open > .dropdown-toggle.btn-bright {
color: #fff;
color: #eee;
background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b;
}
@@ -417,7 +417,7 @@ fieldset[disabled] .btn-dark.active {
.btn-bright:active.focus,
.btn-bright.active.focus,
.open > .dropdown-toggle.btn-bright.focus {
color: #fff;
color: #eee;
background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b;
}
@@ -448,7 +448,7 @@ fieldset[disabled] .btn-bright.active {
border-color: #b56d16;
}
.btn-bright .badge {
color: #fff;
color: #eee;
background-color: #cc7b19;
box-shadow: inset 0 1px 0 #e7993b;
}
@@ -459,22 +459,26 @@ fieldset[disabled] .btn-bright.active {
float: right;
}
.btn-danger.btn-edit:hover {
color: #fff;
color: #eee;
background-color: #c9302c;
border-color: #ac2925;
}
.btn-danger.btn-edit.active {
color: #fff;
color: #eee;
background-color: #c9302c;
border-color: #ac2925;
}
.btn-danger.btn-edit.active:hover {
color: #fff;
color: #eee;
background-color: #ac2925;
border-color: #761c19;
}
.btn-group select {
margin-top: 0;
height: 34px;
}
.btn-group label {
margin-bottom: 0;
}
.input-group-addon-form {
display: inline-block;
@@ -488,9 +492,6 @@ fieldset[disabled] .btn-bright.active {
width: 100%;
margin-top: 5px;
}
#user-selection label {
margin-bottom: 0;
}
.alert-edit {
display: none;
float: left;
@@ -512,7 +513,7 @@ fieldset[disabled] .btn-bright.active {
background-color: #222222;
}
.modal-body table {
color: #fff;
color: #eee;
}
.modal-body li {
margin-top: 7px;
@@ -526,7 +527,7 @@ fieldset[disabled] .btn-bright.active {
color: #E5A00D;
}
.modal-body i.fa {
color: #fff;
color: #eee;
}
.modal-body td:hover a .fa,
.modal-body a:focus i.fa {
@@ -560,7 +561,7 @@ input[type="tel"],
input[type="color"],
.uneditable-input {
margin: 5px 0 5px 0;
color: #fff;
color: #eee;
border: 0px solid #444;
background: #555;
height: 32px;
@@ -572,7 +573,7 @@ input[type="color"],
textarea.form-control {
height: initial;
margin: 5px 0 5px 0;
color: #fff;
color: #eee;
border: 0px solid #444;
background: #555;
padding: 6px 12px;
@@ -584,7 +585,7 @@ textarea.form-control {
textarea.form-control:focus {
outline: 0;
color: #555;
background-color: #fff;
background-color: #eee;
transition: background-color .3s;
}
.pagination > li > a,
@@ -594,7 +595,7 @@ textarea.form-control:focus {
padding: 6px 12px;
margin-left: -1px;
line-height: 1.42857143;
color: #fff;
color: #eee;
text-decoration: none;
background-color: #262626;
border: 1px solid #444444;
@@ -613,7 +614,7 @@ textarea.form-control:focus {
.pagination > .active > a:focus,
.pagination > .active > span:focus {
z-index: 2;
color: #fff;
color: #eee;
cursor: default;
background-color: #cc7b19;
border-color: #444444;
@@ -632,7 +633,7 @@ textarea.form-control:focus {
.nav-pills > li.active > a,
.nav-pills > li.active > a:hover,
.nav-pills > li.active > a:focus {
color: #fff;
color: #eee;
background-color: #cc7b19;
}
.nav-pills > li > a {
@@ -666,11 +667,11 @@ textarea.form-control:focus {
-webkit-appearance:none;
}
.btn-form:hover {
color: #fff;
color: #eee;
background-color: #333;
}
.btn-form:focus {
color: #fff;
color: #eee;
}
.form-control-feedback {
color: #E5A00D;
@@ -682,7 +683,7 @@ fieldset[disabled] .form-control {
background-color: #555;
}
.form-control[readonly]:focus {
background-color: #fff;
background-color: #eee;
}
.poster {
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);
}
.users-poster-face {
overflow: hidden;
float: left;
background-size: cover;
background-position: center;
@@ -857,7 +857,6 @@ a .users-poster-face:hover {
z-index: 2;
}
.dashboard-activity-info-platform {
padding: 6px !important;
background-position: center;
background-size: cover;
width: 50px;
@@ -973,7 +972,7 @@ a .users-poster-face:hover {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 140px;
max-width: 125px;
}
.dashboard-activity-info-time {
position: absolute;
@@ -1032,17 +1031,17 @@ a .users-poster-face:hover {
height: 249px;
}
.dashboard-activity-container:hover .dashboard-activity-progress {
height: 14px;
height: 14px;
}
.dashboard-activity-container:hover .progress-bar {
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: linear-gradient(to left,rgba(0,0,0,0.25) 0%,rgba(0,0,0,0) 50px);
}
.dashboard-activity-container:hover .buffer-bar {
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: 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-weight: bold;
line-height: 25px;
color: #fff;
color: #eee;
}
.dashboard-activity-metadata-play_state-icon {
flex-basis: 25px;
@@ -1536,7 +1535,7 @@ a:hover .dashboard-recent-media-cover {
}
.dashboard-recent-media-metacontainer h3 {
padding: 5px 3px 0 3px;
color: #fff;
color: #eee;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
@@ -1649,12 +1648,12 @@ a:hover .dashboard-recent-media-cover {
color: #f9be03;
}
.summary-content-title h1 a:hover {
color: #fff;
color: #eee;
}
.summary-content-title h2 {
margin-top: 0;
margin-bottom: 10px;
color: #fff;
color: #eee;
font-size: 28px;
line-height: 40px;
float: left;
@@ -1742,7 +1741,7 @@ a:hover .dashboard-recent-media-cover {
top: 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-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: linear-gradient(to bottom,rgba(0,0,0,.7) 0,rgba(0,0,0,.9) 100%);
background-repeat: repeat-x;
@@ -1808,7 +1807,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
line-height: 24px;
}
.summary-content-details-tag strong {
color: #fff;
color: #eee;
margin-left: 2px;
margin-right: 10px;
}
@@ -1828,7 +1827,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
}
.summary-content-summary {
overflow: hidden;
color: #fff;
color: #eee;
float: left;
position: relative;
clear: both;
@@ -1862,7 +1861,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
display: block;
font-size: 12px;
line-height: 18px;
color: #fff;
color: #eee;
}
.summary-content-genres {
margin-top: 13px;
@@ -1881,7 +1880,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
display: block;
font-size: 12px;
line-height: 18px;
color: #fff;
color: #eee;
}
.summary-content-writers {
margin-top: 13px;
@@ -1900,7 +1899,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
display: block;
font-size: 12px;
line-height: 18px;
color: #fff;
color: #eee;
}
.star-rating {
display: inline-block;
@@ -1921,6 +1920,16 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
margin-left: 2px;
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,
.search-results-list {
position: relative;
@@ -1943,7 +1952,7 @@ a:hover .summary-poster-face-track .summary-poster-face-overlay span {
position: relative;
margin: 0;
line-height: 22px;
color: #fff;
color: #eee;
font-size: 16px;
text-align: center;
text-transform: uppercase;
@@ -2039,7 +2048,7 @@ a:hover .item-children-poster {
.item-children-instance-text-wrapper h3 {
width: 100%;
padding: 5px 3px 0 3px;
color: #fff;
color: #eee;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
@@ -2104,7 +2113,7 @@ a:hover .item-children-poster {
margin-right: 20px;
}
#new_title h3 {
color: #f9be03;
color: #E5A00D;
font-size: 14px;
line-height: 1.42857143;
font-weight: bold;
@@ -2140,7 +2149,7 @@ span.settings-warning {
padding-left: 10px;
}
#menu_link_show_advanced_settings.active {
color: #fff;
color: #eee;
background-color: #cc7b19;
}
.advanced-setting {
@@ -2153,7 +2162,7 @@ div.advanced-setting {
li.advanced-setting {
border-left: 1px solid #cc7b19;
}
.docker-setting {
.setting-message {
color: #cc7b19;
margin-left: 10px;
}
@@ -2175,7 +2184,7 @@ li.advanced-setting {
}
.user-info-username {
font-size: 24px;
color: #fff;
color: #eee;
padding-top: 27px;
padding-left: 105px;
}
@@ -2241,7 +2250,7 @@ li.advanced-setting {
left: 0px;
}
.user-overview-stats-instance h3 strong{
color: #fff;
color: #eee;
}
.user-overview-stats-instance h3 {
font-size: 30px;
@@ -2254,7 +2263,7 @@ li.advanced-setting {
float: left;
}
.user-overview-stats-instance h4 {
color: #fff;
color: #eee;
margin-bottom: 25px;
}
.user-overview-stats-instance h1 {
@@ -2294,7 +2303,7 @@ li.advanced-setting {
.user-player-instance-name {
float: left;
padding-top: 14px;
color: #fff;
color: #eee;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
@@ -2304,6 +2313,7 @@ li.advanced-setting {
width: 140px;
margin-left: 10px;
margin-bottom: 10px;
white-space: nowrap;
}
.user-player-instance-playcount h3 {
font-size: 30px;
@@ -2352,9 +2362,6 @@ a .library-user-instance-box:hover {
-moz-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 {
margin: 25px 0;
height: 34px;
@@ -2432,7 +2439,7 @@ a .library-user-instance-box:hover {
overflow: hidden;
}
.home-platforms-instance-name {
color: #fff;
color: #eee;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
@@ -2619,7 +2626,7 @@ a .library-user-instance-box:hover {
}
.home-platforms-instance-list-name {
float: left;
color: #fff;
color: #eee;
text-overflow: ellipsis;
overflow: hidden;
position: relative;
@@ -2986,6 +2993,9 @@ a .home-platforms-list-cover-face:hover
.accordion li .link i.fa {
color: #999;
}
.accordion li .link span.toggle-left {
padding-right: 5px;
}
.accordion li .link span.toggle-right {
float: right;
padding-left: 10px;
@@ -3031,7 +3041,7 @@ a .home-platforms-list-cover-face:hover
}
.submenu a:hover {
background: #f9be03;
color: #FFF;
color: #eee;
}
.ajaxMsg {
background-color: rgba(255,255,255,0.075);
@@ -3090,24 +3100,40 @@ div.dataTables_info {
white-space: normal !important;
}
.tooltip.top .tooltip-arrow {
border-top-color: #fff;
border-top-color: #eee;
}
.tooltip.right .tooltip-arrow {
border-right-color: #fff;
border-right-color: #eee;
}
.tooltip.bottom .tooltip-arrow {
border-bottom-color: #fff;
border-bottom-color: #eee;
}
.tooltip.left .tooltip-arrow {
border-left-color: #fff;
border-left-color: #eee;
}
.tooltip-inner {
max-width: 250px;
color: #000;
background: #fff;
background: #eee;
border: 0;
font-weight: bold;
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 {
z-index: 2000;
padding: 0;
@@ -3152,7 +3178,7 @@ div.dataTables_info {
.channel-thumbnail {
background-color: #868b8b;
background-position: center;
background-size: cover;
background-size: contain;
background-origin: content-box;
background-repeat: no-repeat;
height: 50px;
@@ -3180,7 +3206,7 @@ div.dataTables_info {
}
.edit-user-toggles > input[type='checkbox']:checked + label,
.edit-library-toggles > input[type='checkbox']:checked + label {
color: #fff;
color: #eee;
cursor: pointer;
}
.edit-user-name > input[type='text'] {
@@ -3426,10 +3452,6 @@ pre::-webkit-scrollbar-thumb {
.activity-queue tr:nth-child(even) td {
background-color: rgba(255,255,255,0.010);
}
#days-selection label,
#months-selection label {
margin-bottom: 0;
}
.card-sortable {
height: 36px;
padding: 0 20px 0 0;
@@ -3484,13 +3506,13 @@ pre::-webkit-scrollbar-thumb {
width: 225px;
}
.config-scheduler-table th {
color: #fff;
color: #eee;
}
a.no-highlight {
color: #777;
}
a.no-highlight:hover {
color: #fff;
color: #eee;
}
.top-line {
border-top: 1px dotted #777;
@@ -3498,7 +3520,7 @@ a.no-highlight:hover {
}
.help-bold {
font-weight: bold;
color: #fff;
color: #eee;
}
.save-button {
margin-top: 15px;
@@ -3643,7 +3665,7 @@ a.no-highlight:hover {
margin: 0 2px;
padding: 2px 5px;
font-size: 13px;
color: #fff;
color: #eee;
background-color: #555;
border: 0px solid #444;
border-radius: 3px;
@@ -3661,7 +3683,7 @@ a.no-highlight:hover {
-webkit-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);
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 {
left: 10px;
@@ -3675,7 +3697,7 @@ a.no-highlight:hover {
cursor: pointer;
}
.overlay-refresh-image.info-art:hover {
color: #fff;
color: #eee;
text-shadow: none;
}
a:hover .overlay-refresh-image {
@@ -3692,10 +3714,6 @@ a:hover .overlay-refresh-image:hover {
padding-top: 10px;
padding-bottom: 10px;
}
#plexpy-log-levels label,
#plex-log-levels label {
margin-bottom: 0;
}
#plexpy-notifiers-table .friendly_name,
#notifier-config-modal span.notifier_id,
#plexpy-newsletters-table .friendly_name,
@@ -3730,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:hover,
#newsletter-config-modal .nav-tabs > li.active > a:focus {
color: #fff;
color: #eee;
background: #222;
}
#notifier-config-modal .nav-tabs > li.active > a,
@@ -3797,9 +3815,8 @@ a:hover .overlay-refresh-image:hover {
}
.svg-icon {
padding: 10px;
background-size: calc(100% - 20px) calc(100% - 20px) !important;
background-origin: content-box !important;
background-size: contain !important;
background-repeat: no-repeat !important;
background-position: center !important;
}
@@ -3847,6 +3864,10 @@ a:hover .overlay-refresh-image:hover {
background-color: #31afe1;
background-image: url(../images/platforms/kodi.svg);
}
.platform-lg {
background-color: #a50034;
background-image: url(../images/platforms/lg.svg);
}
.platform-linux {
background-color: #1793d0;
background-image: url(../images/platforms/linux.svg);
@@ -3909,7 +3930,7 @@ a:hover .overlay-refresh-image:hover {
}
.platform-xbmc {
background-color: #3b4872;
background-image: url(../images/platforms/xbmc.svg);
background-image: url(../images/platforms/kodi.svg);
}
.platform-xbox {
background-color: #107c10;
@@ -3948,6 +3969,9 @@ a:hover .overlay-refresh-image:hover {
.platform-kodi-rgba {
background-color: rgba(49, 175, 225, 0.40);
}
.platform-lg-rgba {
background-color: rgba(165, 0, 52, 0.40);
}
.platform-linux-rgba {
background-color: rgba(23, 147, 208, 0.40);
}
@@ -4020,7 +4044,38 @@ a:hover .overlay-refresh-image:hover {
.stats-most_concurrent {
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 {
background-color: transparent !important;
}
@@ -4052,7 +4107,7 @@ a:hover .overlay-refresh-image:hover {
flex-shrink: 0;
}
#info-modal .stream-info-item .sub-value {
color: #fff;
color: #eee;
font-weight: bold;
margin-left: 10px;
text-align: left;
@@ -4075,7 +4130,7 @@ a:hover .overlay-refresh-image:hover {
.stream-info th:first-child {
width: 125px;
height: 30px;
color: #fff;
color: #eee;
font-size: 12px;
text-align: right;
text-transform: uppercase;
@@ -4198,7 +4253,7 @@ a[data-tab-destination] {
transform: translate(-50%, -50%);
}
.iframe-button {
color: #fff;
color: #eee;
border-radius: 20px;
text-align: center;
cursor: pointer;
@@ -4215,7 +4270,7 @@ a[data-tab-destination] {
}
.iframe-button:hover,
.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;
}
.iframe-button:active {
@@ -4247,4 +4302,41 @@ a[data-tab-destination] {
.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

@@ -118,9 +118,7 @@ DOCUMENTATION :: END
<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>
% elif data['media_type'] in ('photo', 'clip'):
% if data['extra_type']:
<div id="poster-${sk}" class="dashboard-activity-poster" style="background-image: url(${page('pms_image_proxy', data['art'].replace('/art', '/thumb') or data['thumb'], data['rating_key'], 300, 450, fallback='poster', refresh=True)});"></div>
% elif data['parent_thumb']:
% if data['parent_thumb']:
<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:
<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>
@@ -143,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>
% 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">
<i class="fa fa-times" style="padding-top: 8px;"></i>
<i class="fa fa-times" style="padding-top: 10px;"></i>
</div>
% endif
</div>
@@ -220,7 +218,7 @@ DOCUMENTATION :: END
<div class="sub-heading">Container</div>
<div class="sub-value" id="transcode_container-${sk}">
% 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:
Direct Play (${data['stream_container'].upper()})
% endif
@@ -276,14 +274,17 @@ DOCUMENTATION :: END
<div class="sub-heading">Subtitle</div>
<div class="sub-value" id="subtitle_decision-${sk}">
% 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':
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':
Direct Stream (${data['subtitle_codec'].upper()})
Direct Stream (${subtitle_codec})
% elif data['stream_subtitle_decision'] == 'burn':
Burn (${data['subtitle_codec'].upper()})
Burn (${subtitle_codec})
% else:
Direct Play (${data['subtitle_codec'].upper() if data['synced_version'] else data['stream_subtitle_codec'].upper()})
Direct Play (${subtitle_codec if data['synced_version'] else data['stream_subtitle_codec'].upper()})
% endif
% else:
None
@@ -305,7 +306,11 @@ DOCUMENTATION :: END
% endif
<span id="location-${sk}">${data['location'].upper()}</span>:
% 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:
<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>
% else:
@@ -392,7 +397,7 @@ DOCUMENTATION :: END
</div>
</div>
<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>
</a>
<div class="dashboard-activity-metadata-title-container">
@@ -403,6 +408,8 @@ DOCUMENTATION :: END
<i class="fa fa-fw fa-pause"></i>&nbsp;
% elif data['state'] == 'buffering':
<i class="fa fa-fw fa-spinner"></i>&nbsp;
% elif data['state'] == 'error':
<i class="fa fa-fw fa-exclamation-triangle"></i>&nbsp;
% endif
</div>
<div class="dashboard-activity-metadata-title">
@@ -514,7 +521,7 @@ DOCUMENTATION :: END
% endif
</div>
<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>

View File

@@ -40,13 +40,22 @@ DOCUMENTATION :: END
<div class="modal-body" id="modal-text">
<fieldset>
<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="col-md-8">
<input type="text" class="form-control" id="custom_thumb_url" name="custom_thumb_url" value="${data['library_thumb']}">
</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 class="checkbox">
<label>
@@ -80,6 +89,7 @@ DOCUMENTATION :: END
// Save library options
$("#save_library").on('click', function () {
var custom_thumb = $("#custom_thumb_url").val();
var custom_art = $("#custom_art_url").val();
var keep_history = 0;
if ($("#keep_history").is(":checked")) {
keep_history = 1;
@@ -90,6 +100,7 @@ DOCUMENTATION :: END
data: {
section_id: '${data["section_id"]}',
custom_thumb: custom_thumb,
custom_art: custom_art,
keep_history: keep_history
},
cache: false,
@@ -104,21 +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>' +
'This is permanent and cannot be undone!';
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(); });
});
$('#undelete-library').click(function () {
var msg = 'Are you sure you want to undelete this user?';
var msg = 'Are you sure you want to undelete this library?';
var url = 'undelete_library';
confirmAjaxCall(url, msg, { section_id: '${data["section_id"]}' }, null, function () { location.reload(); });
});
$(document).ready(function() {
// Move #confirm-modal to parent container
if (!($('#edit-library-modal').next().is('#confirm-modal-purge'))) {
$('#confirm-modal-purge').appendTo($('#edit-library-modal').parent());
}
$('#edit-library-modal > #confirm-modal-purge').remove();
});
</script>
% endif

View File

@@ -134,13 +134,5 @@ DOCUMENTATION :: END
var url = 'undelete_user';
confirmAjaxCall(url, msg, { user_id: '${data["user_id"]}' }, null, function () { location.reload(); });
});
$(document).ready(function() {
// Move #confirm-modal-purge to parent container
if (!($('#edit-user-modal').next().is('#confirm-modal-purge'))) {
$('#confirm-modal-purge').appendTo($('#edit-user-modal').parent());
}
$('#edit-user-modal > #confirm-modal-purge').remove();
});
</script>
% endif

View File

@@ -8,6 +8,13 @@
<%def name="body()">
<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="header-bar">
<span><i class="fa fa-history"></i> History</span>
@@ -185,19 +192,17 @@
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
history_table.draw();
});
}

View File

@@ -104,7 +104,7 @@ DOCUMENTATION :: END
</div>
% elif stat_id == 'top_users':
<% 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>
</a>
% elif stat_id == 'top_platforms':
@@ -122,7 +122,7 @@ DOCUMENTATION :: END
% elif stat_id.startswith('popular'):
<span class="dashboard-stats-stats-units">users</span>
% 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':
<span class="dashboard-stats-stats-units" id="most-concurrent-header-info">streams</span>
% endif
@@ -134,7 +134,7 @@ DOCUMENTATION :: END
<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-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-live="${row.get('live')}">
<div class="sub-list">${loop.index + 1}</div>
<div class="sub-value">
@@ -152,7 +152,7 @@ DOCUMENTATION :: END
</a>
% elif stat_id == 'top_users':
<% 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']}
</a>
% elif stat_id == 'top_platforms':
@@ -183,76 +183,6 @@ DOCUMENTATION :: END
</div>
% endif
% 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 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('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) {
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) + ')');
}
}
$('.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:
<div class="text-muted">No stats to show for the selected period.</div><br>
% endif

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

View File

@@ -2,7 +2,7 @@
<browserconfig>
<msapplication>
<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>
</tile>
</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": [
{
"src": "${http_root}images/favicon/android-chrome-192x192.png?v=2.0.5",
"src": "android-chrome-192x192.png?v=2.0.5",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "${http_root}images/favicon/android-chrome-256x256.png?v=2.0.5",
"src": "android-chrome-256x256.png?v=2.0.5",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#282a2d",
"background_color": "#282a2d",
"display": "standalone"
"display": "standalone",
"orientation": "any"
}

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

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

@@ -179,10 +179,10 @@
<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">Terminate Session</h4>
<h4 class="modal-title">Terminate Stream</h4>
</div>
<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>
<strong>
<span id="terminate-user"></span><br />
@@ -377,6 +377,9 @@
case 'buffering':
state_icon = '<i class="fa fa-fw fa-spinner"></i>&nbsp;';
break;
case 'error':
state_icon = '<i class="fa fa-fw fa-exclamation-triangle"></i>&nbsp;';
break;
default:
state_icon = '<i class="fa fa-fw fa-question-circle"></i>&nbsp;';
}
@@ -431,7 +434,7 @@
var transcode_container = '';
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 {
transcode_container = 'Direct Play (' + s.stream_container.toUpperCase() + ')';
}
@@ -493,14 +496,15 @@
var subtitle_decision = 'None';
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') {
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') {
subtitle_decision = 'Direct Stream (' + s.subtitle_codec.toUpperCase() + ')';
subtitle_decision = 'Direct Stream (' + subtitle_codec + ')';
} else if (s.stream_subtitle_decision === 'burn') {
subtitle_decision = 'Burn (' + s.subtitle_codec.toUpperCase() + ')';
subtitle_decision = 'Burn (' + subtitle_codec + ')';
} else {
subtitle_decision = 'Direct Play (' + ((s.synced_version === '1') ? s.subtitle_codec.toUpperCase() : s.stream_subtitle_codec.toUpperCase()) + ')';
subtitle_decision = 'Direct Play (' + ((s.synced_version === '1') ? subtitle_codec : s.stream_subtitle_codec.toUpperCase()) + ')';
}
}
$('#subtitle_decision-' + key).html(subtitle_decision);
@@ -726,6 +730,88 @@
% endif
</script>
% 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']:
<script>
function getHomeStats(time_range, stats_type) {
@@ -744,6 +830,7 @@
$("#home-stats").html(xhr.responseText);
$('#ajaxMsg').fadeOut();
lockScroll('#home-stats .dashboard-stats-info-scroller');
statsCardCallback();
}
});
}
@@ -783,6 +870,7 @@
data: { },
complete: function (xhr, status) {
$("#library-stats").html(xhr.responseText);
statsCardCallback();
}
});
}

View File

@@ -41,18 +41,18 @@ DOCUMENTATION :: END
from plexpy import notifiers
from plexpy.common import MEDIA_TYPE_HEADERS, MEDIA_FLAGS_AUDIO, MEDIA_FLAGS_VIDEO
from plexpy.helpers import page
from plexpy.helpers import page, get_percent
# Get audio codec file
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):
return file_type
return codec
# Get video codec file
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):
return file_type
return codec
@@ -269,16 +269,33 @@ DOCUMENTATION :: END
<div class="summary-content">
<div class="summary-content-details-wrapper">
% if data['rating']:
<div class="star-rating hidden-xs hidden-sm" title="${data['rating']}">
% for i in range(0,5):
% if round(float(data['rating']) / 2) > i:
<i class="star-icon fa fa-star"></i>
% else:
<i class="star-icon-o fa fa-star-o"></i>
% endif
% endfor
% if data['rating_image']:
% if data['rating_image'].startswith('imdb://'):
<div class="critic-rating hidden-xs hidden-sm" title="${data['rating']}">
<span class="rating-image rating-imdb"><strong>${data['rating']}</strong></span>
</div>
% 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">
% if data['directors']:
Directed by <strong> ${data['directors'][0]}</strong>
@@ -709,19 +726,17 @@ DOCUMENTATION :: END
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function (row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
history_table.draw();
});
}
@@ -848,10 +863,10 @@ DOCUMENTATION :: END
% if data.get('tvmaze_id') or data.get('themoviedb_id') or data.get('musicbrainz_id'):
<script>
$('#delete-lookup-info').on('click', function () {
var msg = 'Are you sure you want to delete the 3rd party API lookup for <strong>' + $(this).data('title') + '</strong>?<br><br>' +
'The info will be looked up again the next time a notification is sent.';
var msg = 'Are you sure you want to delete all the metadata lookup info for <strong>' + $(this).data('title') + '</strong>?' +
'<br /><br />Tautulli will lookup the metadata info again the next time a notification is sent.';
var url = 'delete_lookup_info';
var data = { rating_key: $(this).data('id'), title: $(this).data('title') };
var data = { rating_key: $(this).data('id') };
var callback = function () {
$('#delete-lookup-info').closest('.btn-group').remove();
};

View File

@@ -65,7 +65,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['collection']:
<li>
<a href="${page('info', 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-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':
@@ -90,7 +90,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['movie']:
<li>
<a href="${page('info', 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-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':
@@ -115,7 +115,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['show']:
<li>
<a href="${page('info', 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-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':
@@ -140,7 +140,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['season']:
<li>
<a href="${page('info', 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-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':
@@ -165,7 +165,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['episode']:
<li>
<a href="${page('info', 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-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':
@@ -191,7 +191,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['artist']:
<li>
<a href="${page('info', 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-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':
@@ -215,7 +215,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['album']:
<li>
<a href="${page('info', 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-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':
@@ -240,7 +240,7 @@ DOCUMENTATION :: END
<ul class="item-children-instance list-unstyled">
% for child in data['results_list']['track']:
<li>
<a href="${page('info', 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-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">

View File

@@ -24,7 +24,6 @@
<div id="ip_error" class="col-sm-12 text-muted"></div>
<div class="col-sm-6">
<ul class="list-unstyled">
<li>Continent: <strong><span id="continent"></span></strong></li>
<li>Country: <strong><span id="country"></span></strong></li>
<li>Region: <strong><span id="region"></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>Latitude: <strong><span id="latitude"></span></strong></li>
<li>Longitude: <strong><span id="longitude"></span></strong></li>
<li>Accuracy Radius: <strong><span id="accuracy"></span></strong></li>
</ul>
</div>
<div class="col-sm-12">
@@ -61,8 +59,6 @@
</div>
</div>
<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>
@@ -82,11 +78,11 @@
error: function () {
$('#ip_error').html('<i class="fa fa-exclamation-circle"></i> Internal request failed.').show();
},
success: function (data) {
if ('error' in data) {
$('#ip_error').html('<i class="fa fa-exclamation-circle"></i> ' + data.error).show();
success: function (result) {
if (result.result === 'error') {
$('#ip_error').html('<i class="fa fa-exclamation-circle"></i> ' + result.message).show();
} else {
$('#continent').html(data.continent);
var data = result.data;
$('#country').html(data.country);
$('#region').html(data.region);
$('#city').html(data.city);
@@ -94,7 +90,6 @@
$('#timezone').html(data.timezone);
$('#latitude').html(data.latitude);
$('#longitude').html(data.longitude);
$('#accuracy').html(data.accuracy + ' km');
}
}
});

View File

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

View File

@@ -237,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) {
$.ajax(url);
}
@@ -258,33 +279,31 @@ $.cachedScript = function (url) {
function isPrivateIP(ip_address) {
var defer = $.Deferred();
$.cachedScript('js/ipaddr.min.js').done(function () {
if (ipaddr.isValid(ip_address)) {
var addr = ipaddr.process(ip_address);
if (ipaddr.isValid(ip_address)) {
var addr = ipaddr.process(ip_address);
var rangeList = [];
if (addr.kind() === 'ipv4') {
rangeList = [
ipaddr.parseCIDR('127.0.0.0/8'),
ipaddr.parseCIDR('10.0.0.0/8'),
ipaddr.parseCIDR('172.16.0.0/12'),
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();
}
var rangeList = [];
if (addr.kind() === 'ipv4') {
rangeList = [
ipaddr.parseCIDR('127.0.0.0/8'),
ipaddr.parseCIDR('10.0.0.0/8'),
ipaddr.parseCIDR('172.16.0.0/12'),
ipaddr.parseCIDR('192.168.0.0/16')
];
} 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();
}
@@ -463,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
function humanFileSize(bytes, si) {
var thresh = si ? 1000 : 1024;
function humanFileSize(bytes, si = true) {
//var thresh = si ? 1000 : 1024;
var thresh = 1024; // Always divide by 2^10 but display SI units
if (Math.abs(bytes) < thresh) {
return bytes + ' B';
}

View File

@@ -36,10 +36,10 @@ history_table_options = {
"targets": [0],
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
if (rowData['id'] === null) {
if (rowData['row_id'] === null) {
$(td).html('');
} 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%",
@@ -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>';
} else if (rowData['state'] === 'buffering') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Buffering"><i class="fa fa-spinner fa-fw"></i></span>';
} else if (rowData['state'] === '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') {
state = '<span class="current-activity-tooltip" data-toggle="tooltip" title="Currently Stopped"><i class="fa fa-stop fa-fw"></i></span>';
}
@@ -81,9 +83,9 @@ history_table_options = {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id']) {
$(td).html('<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
} else {
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['user'] + '">' + cellData + '</a>');
}
} else {
$(td).html(cellData);
@@ -141,7 +143,7 @@ history_table_options = {
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>';
} 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') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}
@@ -184,7 +186,9 @@ history_table_options = {
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="' + 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') {
$(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 {
$(td).html('<a href="' + page('info', rowData['rating_key']) + '">' + cellData + '</a>');
}
@@ -317,19 +321,19 @@ history_table_options = {
"rowCallback": function (row, rowData, rowIndex) {
if (rowData['group_count'] == 1) {
// if no grouped rows simply toggle the delete button
if ($.inArray(rowData['id'], history_to_delete) !== -1) {
$(row).find('button[data-id="' + rowData['id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
if ($.inArray(rowData['row_id'], history_to_delete) !== -1) {
$(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
// 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
var group_ids = rowData['group_ids'].split(',').map(Number);
group_ids.forEach(function (id) {
var index = $.inArray(id, history_to_delete);
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');
}
});
}
@@ -353,7 +357,7 @@ $('.history_table').on('click', '> tbody > tr > td.modal-control', function () {
var rowData = row.data();
$.get('get_stream_data', {
row_id: rowData['id'],
row_id: rowData['row_id'],
session_key: rowData['session_key'],
user: rowData['friendly_name']
}).then(function (jqXHR) {
@@ -382,9 +386,9 @@ $('.history_table').on('click', '> tbody > tr > td.delete-control > button', fun
if (rowData['group_count'] == 1) {
// 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) {
history_to_delete.push(rowData['id']);
history_to_delete.push(rowData['row_id']);
} else {
history_to_delete.splice(index, 1);
}
@@ -549,7 +553,7 @@ function createChildTable(row, rowData) {
var childRowData = childRow.data();
$.get('get_stream_data', {
row_id: childRowData['id'],
row_id: childRowData['row_id'],
user: childRowData['friendly_name']
}).then(function (jqXHR) {
$("#info-modal").html(jqXHR);
@@ -576,9 +580,9 @@ function createChildTable(row, rowData) {
var childRowData = childRow.data();
// 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) {
history_to_delete.push(childRowData['id']);
history_to_delete.push(childRowData['row_id']);
} else {
history_to_delete.splice(index, 1);
}

View File

@@ -83,7 +83,7 @@ history_table_modal_options = {
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>';
} 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') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}
@@ -169,7 +169,7 @@ $('.history_table').on('click', 'td.modal-control', function () {
function showStreamDetails() {
$.ajax({
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,
async: true,
complete: function (xhr, status) {

View File

@@ -27,8 +27,8 @@ libraries_list_table_options = {
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<div class="edit-library-toggles">' +
'<button class="btn btn-xs btn-warning delete-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button>&nbsp' +
'<button class="btn btn-xs btn-warning purge-library" data-id="' + rowData['section_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&nbsp' +
'<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['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' +
'</div>');
},
@@ -41,14 +41,16 @@ libraries_list_table_options = {
"targets": [1],
"data": "library_thumb",
"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 (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 {
$(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 {
$(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,
@@ -61,8 +63,8 @@ libraries_list_table_options = {
"data": "section_name",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html('<div data-id="' + rowData['section_id'] + '">' +
'<a href="library?section_id=' + rowData['section_id'] + '">' + cellData + '</a>' +
$(td).html('<div data-id="' + rowData['row_id'] + '">' +
'<a href="' + page('library', rowData['section_id']) + '">' + cellData + '</a>' +
'</div>');
} else {
$(td).html('n/a');
@@ -232,11 +234,11 @@ libraries_list_table_options = {
showMsg(msg, false, false, 0)
},
"rowCallback": function (row, rowData) {
if ($.inArray(rowData['section_id'], libraries_to_delete) !== -1) {
$(row).find('button.delete-library[data-id="' + rowData['section_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
if ($.inArray(rowData['row_id'], libraries_to_delete) !== -1) {
$(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) {
$(row).find('button.purge-library[data-id="' + rowData['section_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
if ($.inArray(rowData['row_id'], libraries_to_purge) !== -1) {
$(row).find('button.purge-library[data-id="' + rowData['row_id'] + '"]').toggleClass('btn-warning').toggleClass('btn-danger');
}
}
}
@@ -277,11 +279,11 @@ $('#libraries_list_table').on('click', 'td.edit-control > .edit-library-toggles
var row = libraries_list_table.row(tr);
var rowData = row.data();
var index_delete = $.inArray(rowData['section_id'], libraries_to_delete);
var index_purge = $.inArray(rowData['section_id'], libraries_to_purge);
var index_delete = $.inArray(rowData['row_id'], libraries_to_delete);
var index_purge = $.inArray(rowData['row_id'], libraries_to_purge);
if (index_delete === -1) {
libraries_to_delete.push(rowData['section_id']);
libraries_to_delete.push(rowData['row_id']);
if (index_purge === -1) {
tr.find('button.purge-library').click();
}
@@ -300,11 +302,11 @@ $('#libraries_list_table').on('click', 'td.edit-control > .edit-library-toggles
var row = libraries_list_table.row(tr);
var rowData = row.data();
var index_delete = $.inArray(rowData['section_id'], libraries_to_delete);
var index_purge = $.inArray(rowData['section_id'], libraries_to_purge);
var index_delete = $.inArray(rowData['row_id'], libraries_to_delete);
var index_purge = $.inArray(rowData['row_id'], libraries_to_purge);
if (index_purge === -1) {
libraries_to_purge.push(rowData['section_id']);
libraries_to_purge.push(rowData['row_id']);
} else {
libraries_to_purge.splice(index_purge, 1);
if (index_delete != -1) {

View File

@@ -51,9 +51,9 @@ sync_table_options = {
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
if (rowData['user_id']) {
$(td).html('<a href="' + page('user', rowData['user_id']) + '>' + cellData + '</a>');
$(td).html('<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
} else {
$(td).html('<a href="' + page('user', null, rowData['user']) + '">' + cellData + '</a>');
$(td).html('<a href="' + page('user', null, rowData['user']) + '" title="' + rowData['username'] + '">' + cellData + '</a>');
}
} else {
$(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 = {
"destroy": true,
"language": {
@@ -21,16 +43,24 @@ user_ip_table_options = {
"columnDefs": [
{
"targets": [0],
"data":"last_seen",
"render": function ( data, type, full ) {
return moment(data, "X").fromNow();
},
"data": "last_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "15%",
"width": "12%",
"className": "no-wrap"
},
{
"targets": [1],
"data": "first_seen",
"render": seenRender,
"createdCell": seenCreatedCell,
"searchable": false,
"width": "12%",
"className": "no-wrap"
},
{
"targets": [2],
"data": "ip_address",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData) {
@@ -44,22 +74,22 @@ user_ip_table_options = {
$(td).html('n/a');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap modal-control-ip"
},
{
"targets": [2],
"targets": [3],
"data": "platform",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
$(td).html(cellData);
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap"
},
{
"targets": [3],
"targets": [4],
"data": "player",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -67,18 +97,18 @@ user_ip_table_options = {
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>';
} 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') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}
$(td).html('<div><a href="#" data-target="#info-modal" data-toggle="modal"><div style="float: left;">' + transcode_dec + '&nbsp;' + cellData + '</div></a></div>');
}
},
"width": "15%",
"width": "12%",
"className": "no-wrap modal-control"
},
{
"targets": [4],
"targets": [5],
"data": "last_played",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== '') {
@@ -119,7 +149,7 @@ user_ip_table_options = {
"className": "datatable-wrap"
},
{
"targets": [5],
"targets": [6],
"data": "play_count",
"searchable": false,
"width": "10%",
@@ -167,7 +197,7 @@ $('.user_ip_table').on('click', 'td.modal-control', function () {
function showStreamDetails() {
$.ajax({
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,
async: true,
complete: function (xhr, status) {

View File

@@ -44,8 +44,8 @@ users_list_table_options = {
"data": null,
"createdCell": function (td, cellData, rowData, row, col) {
$(td).html('<div class="edit-user-toggles">' +
'<button class="btn btn-xs btn-warning delete-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-trash-o fa-fw"></i> Delete</button>&nbsp' +
'<button class="btn btn-xs btn-warning purge-user" data-id="' + rowData['user_id'] + '" data-toggle="button"><i class="fa fa-eraser fa-fw"></i> Purge</button>&nbsp&nbsp&nbsp' +
'<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['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="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>');
@@ -59,10 +59,12 @@ users_list_table_options = {
"targets": [1],
"data": "user_thumb",
"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 === '') {
$(td).html('<a href="' + page('user', 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 {
$(td).html('<a href="' + page('user', 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,
@@ -75,8 +77,8 @@ users_list_table_options = {
"data": "friendly_name",
"createdCell": function (td, cellData, rowData, row, col) {
if (cellData !== null && cellData !== '') {
$(td).html('<div class="edit-user-name" data-id="' + rowData['user_id'] + '">' +
'<a href="' + page('user', rowData['user_id']) + '">' + cellData + '</a>' +
$(td).html('<div class="edit-user-name" data-id="' + rowData['row_id'] + '">' +
'<a href="' + page('user', rowData['user_id']) + '" title="' + rowData['username'] + '">' + cellData + '</a>' +
'<input type="text" class="hidden" value="' + cellData + '">' +
'</div>');
} else {
@@ -140,7 +142,7 @@ users_list_table_options = {
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>';
} 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') {
transcode_dec = '<span class="transcode-tooltip" data-toggle="tooltip" title="Direct Play"><i class="fa fa-play-circle fa-fw"></i></span>';
}
@@ -254,10 +256,10 @@ users_list_table_options = {
},
"rowCallback": function (row, rowData) {
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) {
$(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');
}
}
}
@@ -268,7 +270,7 @@ $('#users_list_table').on('click', 'td.modal-control', function () {
var rowData = row.data();
$.get('get_stream_data', {
row_id: rowData['id'],
row_id: rowData['history_row_id'],
user: rowData['friendly_name']
}).then(function (jqXHR) {
$("#info-modal").html(jqXHR);
@@ -326,11 +328,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto
var row = users_list_table.row(tr);
var rowData = row.data();
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
var index_delete = $.inArray(rowData['row_id'], users_to_delete);
var index_purge = $.inArray(rowData['row_id'], users_to_purge);
if (index_delete === -1) {
users_to_delete.push(rowData['user_id']);
users_to_delete.push(rowData['row_id']);
if (index_purge === -1) {
tr.find('button.purge-user').click();
}
@@ -349,11 +351,11 @@ $('#users_list_table').on('click', 'td.edit-control > .edit-user-toggles > butto
var row = users_list_table.row(tr);
var rowData = row.data();
var index_delete = $.inArray(rowData['user_id'], users_to_delete);
var index_purge = $.inArray(rowData['user_id'], users_to_purge);
var index_delete = $.inArray(rowData['row_id'], users_to_delete);
var index_purge = $.inArray(rowData['row_id'], users_to_purge);
if (index_purge === -1) {
users_to_purge.push(rowData['user_id']);
users_to_purge.push(rowData['row_id']);
} else {
users_to_purge.splice(index_purge, 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="parent_count">Total Seasons / Albums</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="total_plays">Total Plays</th>
<th align="left" id="total_duration">Total Played Duration</th>
@@ -116,14 +116,14 @@
});
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++) {
$('#libraries-to-delete').append('<li>' + $('div[data-id=' + libraries_to_delete[i] + ']').text() + '</li>');
}
}
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++) {
$('#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').one('click', '#confirm-delete', function () {
libraries_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_library',
type: 'POST',
data: { section_id: row },
cache: false,
async: true,
success: function (data) {
var msg = "Library deleted";
showMsg(msg, false, true, 2000);
}
});
$.ajax({
url: 'delete_all_library_history',
type: 'POST',
data: { row_ids: libraries_to_purge.join(',') },
cache: false,
async: true,
success: function (data) {
var msg = "Library history purged";
showMsg(msg, false, true, 2000);
libraries_list_table.draw();
}
});
libraries_to_purge.forEach(function(row, idx) {
$.ajax({
url: 'delete_all_library_history',
type: 'POST',
data: { section_id: row },
cache: false,
async: true,
success: function (data) {
var msg = "Library history purged";
showMsg(msg, false, true, 2000);
}
});
$.ajax({
url: 'delete_library',
type: 'POST',
data: { row_ids: libraries_to_delete.join(',') },
cache: false,
async: true,
success: function (data) {
var msg = "Library deleted";
showMsg(msg, false, true, 2000);
libraries_list_table.draw();
}
});
libraries_list_table.draw();
});
}
@@ -188,7 +185,7 @@
complete: function (xhr, status) {
var result = $.parseJSON(xhr.responseText);
var msg = result.message;
if (result.result == 'success') {
if (result.result === 'success') {
showMsg('<i class="fa fa-check"></i> ' + msg, false, true, 2000, false);
libraries_list_table.draw();
} else {

View File

@@ -61,10 +61,22 @@ DOCUMENTATION :: END
<div class="col-md-12">
<div class="table-card-back">
<div class="user-info-wrapper">
% if data['library_thumb'][:4] == 'http':
<div class="library-info-poster-face" style="background-image: url(${data['library_thumb']});"></div>
% if data['library_thumb'].startswith('http'):
<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:
<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
<div class="user-info-username">
<span class="set-username">${data['section_name']}</span>
@@ -239,9 +251,9 @@ DOCUMENTATION :: END
% else:
<div id="get_file_sizes_message" style="text-align: center; margin-top: 20px; display: none;">
% 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 />
You may leave this page and come back later.
You may leave this page and check back later.
</div>
<div class='table-card-header'>
<div class="header-bar">
@@ -411,6 +423,8 @@ DOCUMENTATION :: END
history_table.draw();
});
$(".inactive-library-tooltip").tooltip();
% if _session['user_group'] == 'admin':
function loadMediaInfoTable() {
// Build media info table
@@ -471,19 +485,17 @@ DOCUMENTATION :: END
$('#deleteCount').text(history_to_delete.length);
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
history_table.draw();
}
});
history_table.draw();
});
}

View File

@@ -35,10 +35,17 @@ DOCUMENTATION :: END
%>
% for section_type in types:
% 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-container">
<div id="library-stats-background-${section_type}" class="dashboard-stats-background" style="background-image: url(${page('pms_image_proxy', '/:/resources/' + section_type + '-fanart.jpg', None, 500, 280, 40, '282828', 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>
% endif
<div class="dashboard-stats-info-container">
<div id="library-stats-title-${section_type}" class="dashboard-stats-info-title">
<h4>${headers[section_type][0]}</h4>
@@ -48,7 +55,8 @@ DOCUMENTATION :: END
<div class="dashboard-stats-info scoller-content">
<ul class="list-unstyled dashboard-stats-info-list">
% 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-value">
<a href="${page('library', section['section_id'])}" title="${section['section_name']}">

View File

@@ -25,11 +25,11 @@ DOCUMENTATION :: END
<div class="user-player-instance">
<li>
% if a['user_id']:
<a href="${page('user', 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>
</a>
<div class=" user-player-instance-name">
<a href="${page('user', 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>
% else:
<div class="library-user-instance-box" style="background-image: url(${a['user_thumb']});"></div>

View File

@@ -24,7 +24,7 @@
<!-- ICONS -->
<!-- Android -->
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials>
<link rel="manifest" href="${http_root}images/favicon/manifest.json?v=2.0.5" crossorigin="use-credentials">
<meta name="theme-color" content="#282a2d">
<!-- Apple -->
<link rel="apple-touch-icon" sizes="180x180" href="${http_root}images/favicon/apple-touch-icon.png?v=2.0.5">

View File

@@ -33,7 +33,7 @@
<label for="friendly_name">OneSignal Device ID</label>
<div class="row">
<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>
<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']):
<li class="mobile-device pointer" data-id="${device['id']}" data-name="${device['device_name']}">
<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>
<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']}">
@@ -117,6 +121,7 @@ DOCUMENTATION :: END
});
$('#api_qr_address').change(function () {
this.value = $.trim(this.value);
var url = $(this).val();
checkQRAddress(url);
@@ -138,4 +143,6 @@ DOCUMENTATION :: END
}
verifiedDevice = true;
})
$('.officail-tooltip').tooltip();
</script>

View File

@@ -123,7 +123,7 @@
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()):
% for key, value in sorted(item['select_options'].items()):
% if key == item['value']:
<option value="${key}" selected>${value}</option>
% else:
@@ -144,7 +144,7 @@
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
% for section, options in item['select_options'].items():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
@@ -325,7 +325,7 @@
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()):
% for key, value in sorted(item['select_options'].items()):
% if key == item['value']:
<option value="${key}" selected>${value}</option>
% else:
@@ -346,7 +346,7 @@
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
% for section, options in item['select_options'].items():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>

View File

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

View File

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

View File

@@ -1,9 +1,9 @@
% if notifier:
<%!
<%
import json
from plexpy import notifiers, users
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']]
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>
% 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>
% 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>
% endif
<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>
<div class="row">
<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 ''}>
% endif
</div>
</div>
<p class="help-block">${item['description'] | n}</p>
@@ -88,7 +97,7 @@
<div class="row">
<div class="col-md-12">
<select class="form-control" id="${item['name']}" name="${item['name']}">
% for key, value in sorted(item['select_options'].iteritems()):
% for key, value in sorted(item['select_options'].items()):
% if key == item['value']:
<option value="${key}" selected>${value}</option>
% else:
@@ -109,7 +118,7 @@
<option value="select-all">Select All</option>
<option value="remove-all">Remove All</option>
% if isinstance(item['select_options'], dict):
% for section, options in item['select_options'].iteritems():
% for section, options in item['select_options'].items():
<optgroup label="${section}">
% for option in sorted(options, key=lambda x: x['text'].lower()):
<option value="${option['value']}">${option['text']}</option>
@@ -211,7 +220,7 @@
% for action in available_notification_actions:
<li>
<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']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div>
@@ -237,7 +246,7 @@
% for action in available_notification_actions:
<li>
<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']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div>
@@ -268,7 +277,7 @@
% for action in available_notification_actions:
<li>
<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']}
<span class="toggle-right"><i class="fa fa-chevron-down"></i></span>
</div>
@@ -313,7 +322,7 @@
<div class="row">
<div class="col-md-12">
<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>
% endfor
</select>
@@ -684,6 +693,15 @@
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
function validateLogic() {
@@ -844,10 +862,8 @@
PNotify.prototype.options.hide = true;
PNotify.prototype.options.delay = $('#browser_auto_hide_delay').val() * 1000;
}
var notification = new PNotify({
title: $('#test_subject').val(),
text: $('#test_body').val()
});
displayPNotify($('#test_subject').val(), $('#test_body').val());
showMsg('<i class="fa fa-check"></i> Notification sent.', false, true, 5000);
}
}

View File

@@ -32,7 +32,7 @@ DOCUMENTATION :: END
% if data != None:
<%
from plexpy.helpers import page
from plexpy.helpers import cast_to_int, page
%>
% if data:
<div class="dashboard-recent-media-row">
@@ -87,7 +87,7 @@ DOCUMENTATION :: END
<a href="${page('info', item['rating_key'])}" title="${item['title']}">${item['title']}</a>
</h3>
<h3 class="text-muted">
${item['child_count']} Seasons
${item['child_count']} Season${'s' if cast_to_int(item['child_count']) > 1 else ''}
</h3>
<h3 class="text-muted">&nbsp;</h3>
</div>
@@ -151,7 +151,7 @@ DOCUMENTATION :: END
<a href="${page('info', item['rating_key'])}" title="Episode ${item['media_index']}">E${item['media_index']}</a>
</h3>
</div>
% elif item['media_type'] == 'album':
% elif item['media_type'] == 'album':
<a href="${page('info', item['rating_key'])}" title="${item['parent_title']}">
<div class="dashboard-recent-media-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')});">
@@ -177,7 +177,7 @@ DOCUMENTATION :: END
</h3>
<h3 class="text-muted">&nbsp;</h3>
</div>
% endif
% endif
</li>
</div>
% endfor

View File

@@ -28,7 +28,7 @@ DOCUMENTATION :: END
</tr>
</thead>
<tbody>
% for job in common.SCHEDULER_LIST:
% for job, job_type in common.SCHEDULER_LIST.items():
% if job in scheduled_jobs:
<%
sched_job = plexpy.SCHED.get_job(job)
@@ -41,12 +41,12 @@ DOCUMENTATION :: END
<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>
% 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>
% if job == 'Check for active sessions':
<td><a class="queue-modal-link" href="#" data-queue="active sessions">${job}</a></td>
<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" href="#" data-queue="recently added">${job}</a></td>
<td><a class="queue-modal-link no-highlight" href="#" data-queue="recently added">${job}</a></td>
% else:
<td>${job}</td>
% endif

View File

@@ -8,7 +8,7 @@
from plexpy.helpers import anon_url, checked
docker_setting = 'disabled' if plexpy.DOCKER else ''
docker_msg = '<span class="docker-setting small">(Controlled by Docker Container)</span>' if plexpy.DOCKER else ''
docker_msg = '<span class="setting-message small">(Controlled by Docker Container)</span>' if plexpy.DOCKER else ''
available_notification_agents = sorted(notifiers.available_notification_agents(), key=lambda k: k['label'].lower())
available_newsletter_agents = sorted(newsletters.available_newsletter_agents(), key=lambda k: k['label'].lower())
@@ -71,6 +71,13 @@
<h3>Version ${common.RELEASE} <small><a id="changelog-modal-link" href="#"><i class="fa fa-info-circle"></i> Changelog</a></small></h3>
</div>
% endif
<div class="padded-header">
<h3>Tautulli News</h3>
</div>
<div id="tautulli-news">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading news...</div>
<br>
</div>
<div class="padded-header">
<h3>Tautulli Configuration</h3>
</div>
@@ -215,12 +222,14 @@
<p class="help-block">Check for Tautulli updates periodically.</p>
</div>
<div id="git_update_options">
% if not plexpy.FROZEN:
<div class="checkbox">
<label>
<input type="checkbox" id="plexpy_auto_update" name="plexpy_auto_update" value="1" ${config['plexpy_auto_update']} ${docker_setting}> Update Automatically ${docker_msg | n}
</label>
<p class="help-block">Update Tautulli automatically if an update is available.</p>
</div>
% endif
<div class="form-group advanced-setting">
<label for="git_token">GitHub API Token</label>
<div class="row">
@@ -265,6 +274,20 @@
</div>
<p class="help-block">Optional: The path to your git environment variable. Leave blank for default.</p>
</div>
<div class="form-group advanced-setting">
<label>Repair Git Install</label>
<p class="help-block">
Attempt to fix updating by resetting your Tautulli installation back to <strong>${common.RELEASE}</strong>.<br />
Note: This will not affect any saved history or settings.
</p>
<div class="row">
<div class="col-md-4">
<div class="btn-group">
<button class="btn btn-form" type="button" id="reset_git_install">Reset</button>
</div>
</div>
</div>
</div>
% endif
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
@@ -434,12 +457,27 @@
</div>
<p class="help-block">Note: Web interface changes require a restart.</p>
% if os.name == 'nt':
% if common.PLATFORM in ('Windows', 'Darwin'):
<%
tray = {'Windows': 'System Tray', 'Darwin': 'Menu Bar'}
tray_disabled = tray_disabled_msg = ''
if common.PLATFORM == 'Darwin':
from plexpy.macos import HAS_PYOBJC
if not HAS_PYOBJC:
tray_disabled = 'disabled'
tray_disabled_msg = '<span class="setting-message small">(Missing pyobjc module)</span>'
%>
<div class="checkbox">
<label>
<input type="checkbox" class="http-settings" name="win_sys_tray" id="win_sys_tray" value="1" ${config['win_sys_tray']}> Enable System Tray Icon
<input type="checkbox" class="http-settings" name="sys_tray_icon" id="sys_tray_icon" value="1" ${config['sys_tray_icon']} ${tray_disabled}> Enable ${tray[common.PLATFORM]} Icon ${tray_disabled_msg | n}
</label>
<p class="help-block">Show Tautulli shortcut in the system tray.</p>
<p class="help-block">Show Tautulli shortcut in the ${tray[common.PLATFORM].lower()}.</p>
</div>
<div class="checkbox">
<label>
<input type="checkbox" name="launch_startup" id="launch_startup" value="1" ${config['launch_startup']}> Launch at System Startup
</label>
<p class="help-block">Start Tautulli automatically after Login.</p>
</div>
% endif
<div class="checkbox">
@@ -530,29 +568,44 @@
<div class="form-group advanced-setting">
<label for="https_cert">HTTPS Certificate</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_cert" name="https_cert" value="${config['https_cert']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_cert_browse" data-toggle="browse" data-filter=".pem" data-target="#https_cert">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL certificate.</p>
<p class="help-block">The location of the SSL certificate in PEM format.</p>
</div>
<div class="form-group advanced-setting">
<label for="https_cert_chain">HTTPS Certificate Chain</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_cert_chain" name="https_cert_chain" value="${config['https_cert_chain']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_cert_chain" name="https_cert_chain" value="${config['https_cert_chain']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_cert_chain_browse" data-toggle="browse" data-filter=".pem" data-target="#https_cert_chain">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL certificate chain.</p>
<p class="help-block">The location of the SSL certificate chain in PEM format.</p>
</div>
<div class="form-group advanced-setting">
<label for="https_key">HTTPS Key</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control http-settings" id="https_key" name="https_key" value="${config['https_key']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="https_key_browse" data-toggle="browse" data-filter=".pem" data-target="#https_key">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">The location of the SSL key.</p>
<p class="help-block">The location of the SSL key in PEM format.</p>
</div>
</div>
@@ -737,7 +790,6 @@
<label>
<input type="checkbox" class="pms-settings" id="pms_url_manual" name="pms_url_manual" value="1" ${config['pms_url_manual']}> Manual Connection
</label>
<span id="cloudManualConnection" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Use the user defined connection details. Do not retrieve the server connection URL automatically.</p>
</div>
<div class="form-group advanced-setting">
@@ -763,10 +815,15 @@
<input type="checkbox" name="server_changed" id="server_changed" value="1" style="display: none;">
<div class="form-group advanced-setting">
<label for="pms_logs_folder">Logs Folder</label>
<label for="pms_logs_folder">Plex Logs Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^[^\~\%]" data-parsley-errors-container="#pms_logs_folder_error" data-parsley-error-message="Shortcuts are not recognized.">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="pms_logs_folder" name="pms_logs_folder" value="${config['pms_logs_folder']}" size="30" data-parsley-trigger="change" data-parsley-pattern="^[^\~\%]" data-parsley-errors-container="#pms_logs_folder_error" data-parsley-error-message="Shortcuts are not recognized.">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="pms_logs_folder_browse" data-toggle="browse" data-filter=".folderonly" data-target="#pms_logs_folder">Browse</button>
</span>
</div>
</div>
<div id="pms_logs_folder_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
@@ -794,7 +851,6 @@
<label>
<input type="checkbox" id="monitor_pms_updates" name="monitor_pms_updates" value="1" ${config['monitor_pms_updates']}> Monitor Plex Updates
</label>
<span id="cloudMonitorUpdates" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<p class="help-block">Enable to have Tautulli check if updates are available for the Plex Media Server.</p>
</div>
<div id="pms_update_options">
@@ -828,14 +884,6 @@
</p>
</div>
</div>
<div class="checkbox">
<label>
<input type="checkbox" id="monitor_remote_access" name="monitor_remote_access" value="1" ${config['monitor_remote_access']}> Monitor Plex Remote Access
</label>
<span id="cloudMonitorRemoteAccess" style="display: none; color: #eb8600; padding-left: 10px;"> Not available for Plex Cloud servers.</span>
<span id="remoteAccessCheck" class="settings-warning"></span>
<p class="help-block">Enable to have Tautulli check if remote access to the Plex Media Server goes down.</p>
</div>
<div class="form-group advanced-setting">
<label for="refresh_users_interval">Users List Refresh Interval</label>
@@ -927,7 +975,7 @@
</div>
<div id="buffer_wait_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The value (in seconds) Tautulli should wait before triggering the next buffer warning. 0 to always trigger.</p>
<p class="help-block">The value (in seconds) Tautulli should wait before triggering the next buffer warning. Set to 0 to always trigger.</p>
</div>
<div class="checkbox advanced-setting">
<label>
@@ -951,6 +999,20 @@
</div>
<p class="help-block">The number of concurrent streams by a single user for Tautulli to trigger a notification. Minimum 2.</p>
</div>
<div class="form-group advanced-setting">
<label for="notify_concurrent_threshold">Continued Session Threshold</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_continued_session_threshold" name="notify_continued_session_threshold" value="${config['notify_continued_session_threshold']}" data-parsley-min="0" data-parsley-trigger="change" data-parsley-errors-container="#notify_continued_session_threshold_error" required>
</div>
<div id="notify_continued_session_threshold_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">
The number of seconds between stopping and starting a new stream to be considered as a continued session. Set to 0 to consider all streams as new sessions.
<br>
Note: The threshold is only used by the "Initial Stream" notification parameter to determine if a stream is the first stream of a continuous streaming session.
</p>
</div>
<div class="padded-header">
<h3>Recently Added Notifications</h3>
@@ -975,14 +1037,28 @@
</p>
</div>
<div class="form-group">
<label for="notify_recently_added_delay">Notification Delay</label>
<label for="notify_recently_added_delay">Recently Added Notification Delay</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_recently_added_delay" name="notify_recently_added_delay" value="${config['notify_recently_added_delay']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_recently_added_delay_error" required>
</div>
<div id="notify_recently_added_delay_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">Set the delay (in seconds) to wait for consecutive recently added items to group together and to allow metadata to be processed before sending the notification. Minimum 60 seconds.</p>
<p class="help-block">Set the delay (in seconds) to wait for consecutive recently added items to group together and to allow metadata to be processed before sending the recently added notification. Minimum 60 seconds, default 300.</p>
</div>
<div class="form-group advanced-setting">
<label>Flush Recently Added</label>
<p class="help-block">
Attempt to fix recently added notifications by flushing out all of the recently added items in the database.<br />
Warning: This will reset all recently added notifications. For emergency use only when recently added notifications are stuck!
</p>
<div class="row">
<div class="col-md-4">
<div class="btn-group">
<button class="btn btn-form" type="button" id="delete_recently_added">Flush</button>
</div>
</div>
</div>
</div>
<!--<div class="checkbox">
<label>
@@ -994,6 +1070,21 @@
</p>
</div>-->
<div class="padded-header">
<h3>Remote Access Notifications</h3>
</div>
<div class="form-group">
<label for="notify_remote_access_threshold">Remote Access Down Threshold</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="notify_remote_access_threshold" name="notify_remote_access_threshold" value="${config['notify_remote_access_threshold']}" size="5" data-parsley-min="60" data-parsley-trigger="change" data-parsley-errors-container="#notify_remote_access_threshold_error" required>
</div>
<div id="notify_remote_access_threshold_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The duration (in seconds) for Plex remote access to be down before sending a notification. Minimum 60, default 60.</p>
</div>
<div class="padded-header">
<h3>Newsletters</h3>
</div>
@@ -1046,10 +1137,15 @@
</p>
</div>
<div class="form-group advanced-setting">
<label for="newsletter_dir">Custom Newsletter Templates Folder</label>
<label for="newsletter_custom_dir">Custom Newsletter Templates Folder</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="newsletter_custom_dir" name="newsletter_custom_dir" value="${config['newsletter_custom_dir']}">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="newsletter_custom_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#newsletter_custom_dir">Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">Optional: Enter the full path to your custom newsletter templates folder. Leave blank for default.</p>
@@ -1057,8 +1153,13 @@
<div class="form-group advanced-setting">
<label for="newsletter_dir">Newsletter Output Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control" id="newsletter_dir" name="newsletter_dir" value="${config['newsletter_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="newsletter_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#newsletter_dir" ${docker_setting}>Browse</button>
</span>
</div>
</div>
</div>
<p class="help-block">Enter the full path to where newsletter files will be saved.</p>
@@ -1225,53 +1326,18 @@
</label>
<p class="help-block">Enable to lookup links to MusicBrainz for music when available.</p>
</div>
<div class="padded-header">
<h3>Geolocation Database</h3>
</div>
<p class="help-block">The GeoLite2 database is used to geolocate IP addresses.</p>
<p class="help-block">
Please see the <a target='_blank' href='${anon_url('https://github.com/%s/%s-Wiki/wiki/3rd-Party-APIs-Guide' % (plexpy.CONFIG.GIT_USER, plexpy.CONFIG.GIT_REPO))}'>3rd Party APIs Guide</a> for instructions on setting up MaxMind.<br>
</p>
<div class="form-group">
<label for="maxmind_license_key">MaxMind License Key</label>
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control" id="maxmind_license_key" name="maxmind_license_key" value="${config['maxmind_license_key']}" data-parsley-trigger="change">
</div>
</div>
<p class="help-block">
Enter your MaxMind License Key to install the GeoLite2 database.
</p>
</div>
<div class="form-group">
<label for="geoip_db">GeoLite2 Database File</label> ${docker_msg | n}
<label for="delete_lookup_info">Delete Lookup Info</label>
<p class="help-block">Delete all cached metadata lookup info in Tautulli.</p>
<div class="row">
<div class="col-md-9">
<div class="input-group">
<input type="text" class="form-control" id="geoip_db" name="geoip_db" value="${config['geoip_db']}" ${docker_setting} data-parsley-trigger="change" data-parsley-pattern=".+\.mmdb$" data-parsley-errors-container="#geoip_db_error" data-parsley-error-message="Must end with '.mmdb'">
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="install_geoip_db">${'Update' if config["geoip_db_installed"] else 'Install'}</button>
<button class="btn btn-form" type="button" id="uninstall_geoip_db" ${'disabled' if not config['geoip_db_installed'] else ''}>Uninstall</button>
</span>
<div class="btn-group">
<button class="btn btn-form delete_all_lookups" type="button" data-service="themoviedb">TheMovieDB</button>
<button class="btn btn-form delete_all_lookups" type="button" data-service="tvmaze">TVmaze</button>
<button class="btn btn-form delete_all_lookups" type="button" data-service="musicbrainz">MusicBrainz</button>
</div>
</div>
<div id="geoip_db_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">
Leave blank to install in the default location. GeoLite2 database last updated <strong><span id="geoip_db_updated">never</span></strong>.
</p>
</div>
<div class="form-group advanced-setting">
<label for="geoip_db_update_days">GeoLite2 Database Update Interval</label>
<div class="row">
<div class="col-md-2">
<input type="text" class="form-control" data-parsley-type="integer" id="geoip_db_update_days" name="geoip_db_update_days" value="${config['geoip_db_update_days']}" size="5" data-parsley-range="[7, 30]" data-parsley-trigger="change" data-parsley-errors-container="#geoip_db_update_days_error" required>
</div>
<div id="geoip_db_update_days_error" class="alert alert-danger settings-alert" role="alert"></div>
</div>
<p class="help-block">The interval (in days) Tautulli will automatically update the GeoLite2 database. Minimum 7, maximum 30, default 30.</p>
</div>
<p><input type="button" class="btn btn-bright save-button" value="Save" data-success="Changes saved successfully"></p>
@@ -1281,13 +1347,32 @@
<div role="tabpanel" class="tab-pane" id="tabs-import_backups">
<div class="padded-header">
<h3>Database Import</h3>
<h3>Import</h3>
</div>
<p class="help-block">Click a button below to import an existing database from another app.</p>
<div class="btn-group">
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button>
<div class="form-group">
<label for="database_import">Database Import</label>
<p class="help-block">Click a button below to import an existing database from the selected app.</p>
<div class="row">
<div class="col-md-9">
<div class="btn-group">
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="tautulli">Tautulli</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexwatch">PlexWatch</button>
<button class="btn btn-form toggle-app-import-modal" type="button" data-target="#app-import-modal" data-toggle="modal" data-app="plexivity">Plexivity</button>
</div>
</div>
</div>
</div>
<div class="form-group">
<label for="config_import">Configuration Import</label>
<p class="help-block">Click the button below to import a previous Tautulli configuration.</p>
<div class="row">
<div class="col-md-9">
<div class="btn-group">
<button class="btn btn-form toggle-config-import-modal" type="button" data-target="#config-import-modal" data-toggle="modal">Tautulli</button>
</div>
</div>
</div>
</div>
<div class="padded-header">
@@ -1325,8 +1410,13 @@
<div class="form-group">
<label for="log_dir">Log Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="log_dir" name="log_dir" value="${config['log_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="log_dir" name="log_dir" value="${config['log_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="log_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#log_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_logs">Clear Logs</button>
</div>
@@ -1336,8 +1426,13 @@
<div class="form-group">
<label for="backup_dir">Backup Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="backup_dir" name="backup_dir" value="${config['backup_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="backup_dir" name="backup_dir" value="${config['backup_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="backup_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#backup_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="backup_config">Backup Config</button>
<button class="btn btn-form" type="button" id="backup_database">Backup Database</button>
@@ -1348,8 +1443,13 @@
<div class="form-group">
<label for="cache_dir">Cache Directory</label> ${docker_msg | n}
<div class="row">
<div class="col-md-6">
<input type="text" class="form-control directory-settings" id="cache_dir" name="cache_dir" value="${config['cache_dir']}" ${docker_setting}>
<div class="col-md-7">
<div class="input-group">
<input type="text" class="form-control directory-settings" id="cache_dir" name="cache_dir" value="${config['cache_dir']}" ${docker_setting}>
<span class="input-group-btn">
<button class="btn btn-form" type="button" id="cache_dir_browse" data-toggle="browse" data-filter=".folderonly" data-target="#cache_dir" ${docker_setting}>Browse</button>
</span>
</div>
<div class="btn-group">
<button class="btn btn-form" type="button" id="clear_cache">Clear All Cache</button>
<button class="btn btn-form" type="button" id="clear_image_cache">Clear Image Cache</button>
@@ -1381,6 +1481,7 @@
<label>Registered Devices</label>
<p class="help-block">Register a new device using a QR code, or configure an existing device by clicking the settings icon on the right.</p>
<p id="app_api_msg" style="color: #eb8600;">Warning: The API must be enabled under <a data-tab-destination="web_interface" data-target="api_enabled">Web Interface</a> to use the app.</p>
<br />
<div class="row">
<div id="plexpy-mobile-devices-table" class="col-md-12">
<div class='text-muted'><i class="fa fa-refresh fa-spin"></i> Loading registered devices...</div>
@@ -1477,6 +1578,7 @@
</div>
</div>
<div id="app-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="app-import-modal"></div>
<div id="config-import-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="config-import-modal"></div>
<div id="add-notifier-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="add-notifier-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
@@ -1816,7 +1918,10 @@ Rating: {rating}/10 --> Rating: /10
<label>Instructions</label>
<p class="help-block">
Scan the QR code below with the Tautulli Android app to automatically register it with the server (make sure the Tautulli Address below is correct)
or manually enter the connection info and device token into the app settings.
or manually enter the connection info and device token into the app settings. This window will automatically close once device registration is successful.
</p>
<p class="help-block">
Note: OneSignal.com must not be blocked (e.g. in Pi-hole) for device registration.
</p>
<label>QR Code</label>
<pre id="api_qr_code" style="text-align: center"></pre>
@@ -1828,7 +1933,7 @@ Rating: {rating}/10 --> Rating: /10
</p>
<p class="help-block" id="api_qr_private" style="display: none;">
Note: This is a private IP address. Tautulli will not be reachable outside of your home network.
Access Tautulli via an externally address or manually enter the address above to generate the QR code for remote access.
Access Tautulli via an external address or manually enter the address above to generate the QR code for remote access.
</p>
<p class="help-block" id="api_qr_https" style="display: none;">
Note: This URL is not secure. Requests between the app and the server will not be encrypted.
@@ -1844,6 +1949,38 @@ Rating: {rating}/10 --> Rating: /10
</div>
</div>
<div id="mobile-device-config-modal" class="modal fade wide" tabindex="-1" role="dialog" aria-labelledby="mobile-device-config-modal"></div>
<div id="browse-path-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="browse-path-modal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><i class="fa fa-remove"></i></button>
<h4 class="modal-title">File Browser</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="browse-path">Select a <span id="browse-path-type"></span> Below</label>
<div class="row">
<div class="col-md-12">
<input type="text" class="form-control" id="browse-path" name="browse-path" value="" size="30" disabled>
</div>
</div>
</div>
</div>
<div class="col-md-12" style="height: 400px; overflow: auto;">
<ul id="browse-path-list" class="stacked-configs list-unstyled">
</ul>
</div>
</div>
</div>
<div class="modal-footer">
<span id="browse-path-status-message" style="padding-right: 25px;"></span>
<input type="button" id="select-browse-file" class="btn btn-bright" value="Select">
</div>
</div>
</div>
</div>
</%def>
<%def name="javascriptIncludes()">
@@ -1950,6 +2087,54 @@ Rating: {rating}/10 --> Rating: /10
});
}
$("#browse-path-modal").on('hidden.bs.modal', function() {
$("#select-browse-file").unbind('click');
});
function openBrowsePath(key, path, filter_ext, file_description, select_target) {
$("#browse-path-type").text(file_description);
$("#browse-path-modal").modal('show');
$("#select-browse-file").click(function () {
$("#browse-path-modal").modal('hide');
$(select_target).val($("#browse-path").val()).change();
});
browsePath(key, path, filter_ext);
}
function browsePath(key, path, filter_ext) {
$("#browse-path-status-message").html('<i class="fa fa-fw fa-spin fa-refresh"></i>');
getBrowsePath(key, path, filter_ext).then(function (data) {
if (data.result === 'error') {
$("#browse-path-status-message").html("<i class='fa fa-exclamation-triangle'></i> " + data.message);
} else {
$("#browse-path-status-message").html("");
$('#browse-path').val(data.path);
var browse_list = $('#browse-path-list');
browse_list.parent().animate({ scrollTop: 0 }, 0);
browse_list.empty();
$.each(data.data, function(i, item) {
var browse_item = $('<li/>')
.html("<span><i class='fa fa-fw fa-" + item.icon + "'></i>&nbsp; " + item.title + "</span>")
.addClass(item.type + ' pointer')
.data('key', item.key)
.data('path', item.path)
.appendTo(browse_list)
});
$('#browse-path-list li').click(function (){
$('#browse-path').val($(this).data('path'));
if ($(this).hasClass('folder')) {
browsePath($(this).data('key'), null, filter_ext)
}
});
}
});
}
$(document).ready(function() {
// Javascript to enable link to tab
@@ -2129,11 +2314,17 @@ $(document).ready(function() {
});
$("#delete_temp_sessions").click(function () {
var msg = 'Are you sure you want to flush the temporary sessions?<br /><strong>This will reset all currently active sessions.</strong>';
var msg = 'Are you sure you want to flush the temporary sessions?<br /><br /><strong>This will reset all currently active sessions.</strong>';
var url = 'delete_temp_sessions';
confirmAjaxCall(url, msg);
});
$("#delete_recently_added").click(function () {
var msg = 'Are you sure you want to flush the recently added items?<br /><br /><strong>This will reset all recently added notifications.</strong>';
var url = 'delete_recently_added';
confirmAjaxCall(url, msg);
});
$("#switch_git_branch").click(function () {
var current_remote = "${config['git_remote']}";
var current_branch = "${config['git_branch']}";
@@ -2154,6 +2345,17 @@ $(document).ready(function() {
}
});
$("#reset_git_install").click(function () {
var msg = 'Are you sure you want to reset your Tautulli installtion back to <strong>${common.RELEASE}</strong>?' +
'<br /><br />Tautulli will restart.';
$('#confirm-message').html(msg);
$('#confirm-modal').modal();
$('#confirm-modal').one('click', '#confirm-button', function () {
settingsChanged = false;
window.location.href = 'reset_git_install';
});
});
$('#api_key').click(function(){ $('#api_key').select() });
$("#generate_api").click(function() {
$.get('generate_api_key',
@@ -2264,7 +2466,6 @@ $(document).ready(function() {
$('#pms_is_cloud').val(is_cloud !== 'undefined' && is_cloud === true ? 1 : 0);
$('#pms_url_manual').prop('checked', false);
$('#pms_url').val('Please verify your server above to retrieve the URL');
PMSCloudCheck();
},
onDropdownOpen: function() {
this.clear();
@@ -2295,38 +2496,6 @@ $(document).ready(function() {
}
getServerOptions();
function PMSCloudCheck() {
if ($('#pms_is_cloud').val() === "1") {
$('#pms_port').val(443).prop('readonly', true);
$('#pms_is_remote_checkbox').prop('checked', true).prop('disabled', true);
$('#pms_is_remote').val(1);
$('#pms_ssl_checkbox').prop('checked', true).prop('disabled', true);
$('#pms_ssl').val(1);
$('#pms_url_manual').prop('checked', false).prop('disabled', true);
$('#monitor_pms_updates').prop('checked', false).prop('disabled', true);
$('#pms_update_options').hide();
$('#monitor_remote_access').prop('checked', false).prop('disabled', true);
$('#cloudManualConnection').show();
$('#cloudMonitorUpdates').show();
$('#cloudMonitorRemoteAccess').show();
$('#remoteAccessCheck').hide();
} else {
$('#pms_port').prop('readonly', false);
$('#pms_is_remote_checkbox').prop('disabled', false);
$('#pms_is_remote').val($('#pms_is_remote_checkbox').is(':checked') ? 1 : 0);
$('#pms_ssl_checkbox').prop('disabled', false);
$('#pms_ssl').val($('#pms_ssl_checkbox').is(':checked') ? 1 : 0);
$('#pms_url_manual').prop('disabled', false);
$('#monitor_pms_updates').prop('disabled', false);
$('#monitor_remote_access').prop('disabled', false);
$('#cloudManualConnection').hide();
$('#cloudMonitorUpdates').hide();
$('#cloudMonitorRemoteAccess').hide();
remoteAccessEnabledCheck()
}
}
PMSCloudCheck();
function verifyServer(_callback) {
var pms_ip = $("#pms_ip").val();
var pms_port = $("#pms_port").val();
@@ -2414,9 +2583,7 @@ $(document).ready(function() {
$("#token_verify").html('<i class="fa fa-refresh fa-spin"></i>').fadeIn('fast');
}
function OAuthSuccessCallback(authToken) {
var x_plex_headers = getPlexHeaders();
$("#pms_token").val(authToken);
$("#pms_uuid").val(x_plex_headers['X-Plex-Client-Identifier']);
$("#token_verify").html('<i class="fa fa-check"></i>').fadeIn('fast');
getServerOptions(authToken);
}
@@ -2441,25 +2608,22 @@ $(document).ready(function() {
});
});
// Load config import modal
$(".toggle-config-import-modal").click(function() {
$.ajax({
url: 'import_config_tool',
cache: false,
async: true,
complete: function(xhr, status) {
$("#config-import-modal").html(xhr.responseText);
}
});
});
pms_version = false;
pms_logs_debug = false;
pms_logs = false;
function remoteAccessEnabledCheck() {
$.ajax({
url: 'get_server_pref',
data: { pref: 'PublishServerOnPlexOnlineKey' },
async: true,
success: function(data) {
if (data === 'false' || data === '0') {
$("#remoteAccessCheck").html("Remote access must be enabled on your Plex Server. <a target='_blank' href='${anon_url('https://support.plex.tv/hc/en-us/articles/200484543-Enabling-Remote-Access-for-a-Server')}'>Click here</a> for help.");
$("#monitor_remote_access").attr("checked", false).attr("disabled", true);
}
}
});
}
remoteAccessEnabledCheck();
// Sortable home_sections
function set_home_sections() {
var home_sections = [];
@@ -2781,12 +2945,23 @@ $(document).ready(function() {
var name = image_hosting_option.text();
var msg = 'Are you sure you want to delete all uploaded images on <strong>' + name + '</strong>?' +
'<br />All previous links to the images will no longer work. This cannot be undone!';
'<br /><br />All previous links to the images will no longer work. This cannot be undone!';
var url = 'delete_hosted_images';
var data = { service: name, delete_all: true };
confirmAjaxCall(url, msg, data, false);
});
$('body').on('click', '.delete_all_lookups', function () {
var service = $(this).data('service');
var name = $(this).text();
var msg = 'Are you sure you want to delete all the metadata lookup info from <strong>' + name + '</strong>?' +
'<br /><br />Tautulli will lookup the metadata info again the next time a notification is sent.';
var url = 'delete_lookup_info';
var data = { service: service, delete_all: true };
confirmAjaxCall(url, msg, data, false);
});
function baseURLSet() {
if ($('#http_base_url').val()) {
$('.base-url-warning').hide();
@@ -2860,54 +3035,54 @@ $(document).ready(function() {
openPlexXML('/api/resources', true, {includeHttps: 1});
});
if ("${kwargs.get('install_geoip')}" === 'true') {
gotoSetting('3rd_party_apis', 'geoip_db')
}
if ("${config['geoip_db_installed']}" > "0") {
$("#geoip_db_updated").text(moment("${config['geoip_db_installed']}", "X").fromNow());
}
$("#install_geoip_db").click(function () {
var maxmind_license_key = $("#maxmind_license_key");
maxmind_license_key.val($.trim(maxmind_license_key.val()));
if (maxmind_license_key.val() === "") {
maxmind_license_key.focus();
showMsg('<i class="fa fa-exclamation-circle"></i> Maxmind License Key is required.', false, true, 5000, true);
return false;
} else if (!(saveSettings())) {
return false;
}
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.<br />';
var url = 'install_geoip_db';
if ($(this).text() === 'Update') {
url += '?update=true';
}
confirmAjaxCall(url, msg, null, 'Installing GeoLite2 database.', function (result) {
if (result.result === "success") {
$('#install_geoip_db').text('Update');
$('#uninstall_geoip_db').prop('disabled', false);
$('#geoip_db_updated').text(moment(result.updated, "X").fromNow());
var tautulli_news = $('#tautulli-news')
$.ajax({
url: 'https://tautulli.com/news/tautulli-news.json',
type: 'GET',
dataType: 'json',
cache: false,
async: true,
success: function (data) {
if (data) {
var now = moment().endOf('day');
var news = $('<ul/>').addClass('accordion list-unstyled')
$.each(data, function (index, news_item) {
var date = moment(news_item.date, "YYYY-MM-DD");
if (index >= 5) { return false; }
var header = $('<div/>').addClass('link').html(
'<span class="toggle-left"><i class="fa fa-newspaper fa-fw"></i></span>' +
'<span class="news-title">' + news_item.title + '</span>' +
'<span class="toggle-right"><i class="fa fa-chevron-down fa-fw"></i></span>' +
'<span class="news-date toggle-right">' + date.format($('#date_format').val()) + '</span>');
var subtitle = $('<span/>').addClass('news-subtitle').html(news_item.subtitle);
var body = $('<span/>').addClass('news-body').html(news_item.body);
var content = $('<div/>').addClass('submenu');
if (news_item.subtitle) { content.append(subtitle); }
content.append(body);
var li = $('<li/>').append(header).append(content)
if (index === 0 && Math.abs(now.diff(date, 'days')) < 7) {
li.addClass('open');
content.css('display', 'block');
}
news.append(li)
});
tautulli_news.html(news);
var accordion_news = new Accordion(news, false);
} else {
tautulli_news.html('<p class="help-block"><i class="fa fa-check"></i>&nbsp; No news available.</p>')
}
getSchedulerTable();
});
},
error: function () {
tautulli_news.html('<p class="help-block"><i class="fa fa-exclamation-triangle"></i>&nbsp; Failed to retrieve news.</p>')
}
});
$("#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.', function (result) {
if (result.result === "success") {
$('#install_geoip_db').text('Install');
$('#uninstall_geoip_db').prop('disabled', true);
$('#geoip_db_updated').text('never');
}
getSchedulerTable();
});
$("body").on('click', '[data-toggle=browse]', function () {
var filter = $(this).data('filter');
var target = $(this).data('target');
var path = $(target).val();
var description = $(this).data('description') || $("label[for='" + target.replace('#', '') + "']").text();
openBrowsePath(null, path, filter, description, target);
});
});
</script>

View File

@@ -22,10 +22,10 @@
<div class="modal-body" id="modal-text">
<div align="center">
% 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 />
% 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 />
<h4>Restart in <span class="countdown"></span></h4>
% endif

View File

@@ -39,30 +39,43 @@ DOCUMENTATION :: END
<ul class="list-unstyled breadcrumb">
% if query['media_type'] == 'movie':
<li>Movies</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['title']}</li>
% elif query['media_type'] == 'show':
<li>TV Shows</li>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['grandparent_title']}</li>
% elif query['media_type'] == 'season':
<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>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">Season ${query['parent_media_index']}</li>
% elif query['media_type'] == 'episode':
<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>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<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>
% 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>
% elif query['media_type'] == 'album':
<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>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<li class="active">${query['parent_title']}</li>
% elif query['media_type'] == 'track':
<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>
<span class="breadcrumb-arrow"><i class="fa fa-chevron-right"></i></span>
<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>
% endif
</ul>
@@ -158,6 +171,7 @@ DOCUMENTATION :: END
</p>
<p> with </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':
<p>All items for <strong>${query['grandparent_title']}</strong> will also be updated.</p>
% endif
@@ -198,10 +212,12 @@ DOCUMENTATION :: END
$(document).on('click', '#search-results-list a', function (e) {
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');
$('#new_title').html($(this).find('.item-children-instance-text-wrapper').html());
$('#new_library').text(new_library_section);
$('#confirm-modal-update').modal();
$('#confirm-modal-update').one('click', '#confirm-update', function () {

View File

@@ -51,7 +51,13 @@ DOCUMENTATION :: END
<div class="col-md-12">
<div class="table-card-back">
<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">
<span class="set-username">${data['friendly_name']}</span>
% if _session['user_group'] == 'admin':
@@ -278,7 +284,8 @@ DOCUMENTATION :: END
<table class="display user_ip_table" id="user_ip_table-UID-${data['user_id']}" width="100%">
<thead>
<tr>
<th align="left" id="last_seen">Last Seen</th>
<th align="left" id="last_seen">Last Streamed</th>
<th align="left" id="first_seen">First Streamed</th>
<th align="left" id="ip_address">IP Address</th>
<th align="left" id="platform">Last Platform</th>
<th align="left" id="player">Last Player</th>
@@ -540,6 +547,8 @@ DOCUMENTATION :: END
login_log_table.draw();
});
$(".inactive-user-tooltip").tooltip();
% if _session['user_group'] == 'admin':
$("#edit-user-tooltip").tooltip();
@@ -566,19 +575,17 @@ DOCUMENTATION :: END
$('#deleteType').text('history');
$('#confirm-modal-delete').modal();
$('#confirm-modal-delete').one('click', '#confirm-delete', function () {
history_to_delete.forEach(function(row, idx) {
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_id: row },
async: true,
success: function (data) {
var msg = "History deleted";
showMsg(msg, false, true, 2000);
}
});
$.ajax({
url: 'delete_history_rows',
type: 'POST',
data: { row_ids: history_to_delete.join(',') },
async: true,
success: function (data) {
var msg = "History deleted";
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 class="user-player-instance-box svg-icon platform-${a['platform_name']}"></div>
</div>
<div class="user-player-instance-name">
<div class="user-player-instance-name" title="${a['player_name']}">
${a['player_name']}
</div>
<div class="user-player-instance-playcount">

View File

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

View File

@@ -203,7 +203,7 @@
<h3>Database Import</h3>
<div class="wizard-input-section">
<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 class="help-block">
To import a database, navigate to the <strong>Settings</strong> page
@@ -216,6 +216,8 @@
<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="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="api_enabled" id="api_enabled" value="1" checked>
<input type="checkbox" name="refresh_users_on_startup" id="refresh_users_on_startup" value="1" checked>
@@ -223,6 +225,8 @@
<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="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="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">
@@ -491,7 +495,7 @@ $(document).ready(function() {
var pms_ssl = $("#pms_ssl").val();
var pms_is_remote = $("#pms_is_remote").val();
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');
$.ajax({
url: 'get_server_id',
@@ -506,7 +510,7 @@ $(document).ready(function() {
async: true,
timeout: 5000,
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');
},
success: function(xhr, status) {

View File

@@ -26,6 +26,7 @@
<meta name="viewport" content="width=device-width">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Tautulli Newsletter - ${subject}</title>
<link rel="shortcut icon" href="${base_url_image + 'images/favicon/favicon.ico' if base_url_image else 'https://tautulli.com/images/favicon.ico'}">
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@@ -26,6 +26,7 @@
<meta name="viewport" content="width=device-width"/>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<title>Tautulli Newsletter - ${subject}</title>
<link rel="shortcut icon" href="${base_url_image + 'images/favicon/favicon.ico' if base_url_image else 'https://tautulli.com/images/favicon.ico'}">
<style>
/* -------------------------------------
GLOBAL RESETS

View File

@@ -1,88 +0,0 @@
#!/bin/sh
#
# PROVIDE: tautulli
# REQUIRE: tautulli
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf.local or /etc/rc.conf
# to enable this service:
#
# tautulli_enable (bool): Set to NO by default.
# Set it to YES to enable it.
# tautulli_user: The user account Tautulli daemon runs as what
# you want it to be. It uses 'tautulli' user by
# default. Do not sets it as empty or it will run
# as root.
# tautulli_dir: Directory where Tautulli lives.
# Default: /usr/local/share/Tautulli
# tautulli_chdir: Change to this directory before running Tautulli.
# Default is same as tautulli_dir.
# tautulli_pid: The name of the pidfile to create.
# Default is tautulli.pid in tautulli_dir.
. /etc/rc.subr
name="tautulli"
rcvar=${name}_enable
load_rc_config ${name}
: ${tautulli_enable:="NO"}
: ${tautulli_user:="tautulli"}
: ${tautulli_dir:="/usr/local/share/Tautulli"}
: ${tautulli_chdir:="${tautulli_dir}"}
: ${tautulli_pid:="${tautulli_dir}/tautulli.pid"}
: ${tautulli_conf:="${tautulli_dir}/config.ini"}
WGET="/usr/local/bin/wget" # You need wget for this script to safely shutdown Tautulli.
if [ -e "${tautulli_conf}" ]; then
HOST=`grep -A64 "\[General\]" "${tautulli_conf}"|egrep "^http_host"|perl -wple 's/^http_host = (.*)$/$1/'`
PORT=`grep -A64 "\[General\]" "${tautulli_conf}"|egrep "^http_port"|perl -wple 's/^http_port = (.*)$/$1/'`
fi
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --quiet --nolaunch --port ${PORT} --pidfile ${tautulli_pid} --config ${tautulli_conf}"
# Check for wget and refuse to start without it.
if [ ! -x "${WGET}" ]; then
warn "Tautulli not started: You need wget to safely shut down Tautulli."
exit 1
fi
# Ensure user is root when running this script.
if [ `id -u` != "0" ]; then
echo "Oops, you should be root before running this!"
exit 1
fi
verify_tautulli_pid() {
# Make sure the pid corresponds to the Tautulli process.
pid=`cat ${tautulli_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python ${tautulli_dir}/Tautulli.py"
return $?
}
# Try to stop Tautulli cleanly by calling shutdown over http.
tautulli_stop() {
if [ ! -e "${tautulli_conf}" ]; then
echo "Tautulli' settings file does not exist. Try starting Tautulli, as this should create the file."
exit 1
fi
echo "Stopping $name"
verify_tautulli_pid
${WGET} -O - -q --user=${SBUSR} --password=${SBPWD} "http://${HOST}:${PORT}/shutdown/" >/dev/null
if [ -n "${pid}" ]; then
wait_for_pids ${pid}
echo "Stopped $name"
fi
}
tautulli_status() {
verify_tautulli_pid && echo "$name is running as ${pid}" || echo "$name is not running"
}
run_rc_command "$1"

View File

@@ -1,76 +0,0 @@
#!/bin/sh
#
### BEGIN INIT INFO
# Provides: Tautulli
# Required-Start: $all
# Required-Stop: $all
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts Tautulli
# Description: starts Tautulli
### END INIT INFO
# Source function library.
. /etc/init.d/functions
## Variables
prog=tautulli
lockfile=/var/lock/subsys/$prog
homedir=/opt/Tautulli
datadir=/opt/Tautulli
configfile=/opt/Tautulli/config.ini
pidfile=/var/run/tautulli.pid
nice=
# The following line must point to your Python 2.7 install
python27=/usr/src/Python-2.7.11/python
##
options=" --daemon --config $configfile --pidfile $pidfile --datadir $datadir --nolaunch --quiet"
start() {
# Start daemon.
echo -n $"Starting $prog: "
daemon --pidfile=$pidfile $nice $python27 $homedir/Tautulli.py $options
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && touch $lockfile
return $RETVAL
}
stop() {
echo -n $"Shutting down $prog: "
killproc -p $pidfile $python27
RETVAL=$?
echo
[ $RETVAL -eq 0 ] && rm -f $lockfile
return $RETVAL
}
# See how we were called.
case "$1" in
start)
start
;;
stop)
stop
;;
status)
status $prog
;;
restart|force-reload)
stop
start
;;
try-restart|condrestart)
if status $prog > /dev/null; then
stop
start
fi
;;
reload)
exit 3
;;
*)
echo $"Usage: $0 {start|stop|status|restart|try-restart|force-reload}"
exit 2
esac

View File

@@ -38,6 +38,7 @@ load_rc_config ${name}
status_cmd="${name}_status"
stop_cmd="${name}_stop"
command_interpreter="python"
command="${tautulli_dir}/Tautulli.py"
command_args="--daemon --pidfile ${tautulli_pid} --quiet --nolaunch ${tautulli_flags}"
@@ -51,7 +52,7 @@ verify_tautulli_pid() {
# Make sure the pid corresponds to the Tautulli process.
if [ -f ${tautulli_pid} ]; then
pid=`cat ${tautulli_pid} 2>/dev/null`
ps -p ${pid} | grep -q "python2 ${tautulli_dir}/Tautulli.py"
ps -p ${pid} | grep -q "python ${tautulli_dir}/Tautulli.py"
return $?
else
return 0
@@ -60,7 +61,7 @@ verify_tautulli_pid() {
# Try to stop Tautulli cleanly by sending SIGTERM
tautulli_stop() {
echo "Stopping $name"
echo "Stopping $name."
verify_tautulli_pid
if [ -n "${pid}" ]; then
kill ${pid}

View File

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

1
init-scripts/init.freenas Symbolic link
View File

@@ -0,0 +1 @@
init.freebsd

View File

@@ -1,47 +0,0 @@
<?xml version="1.0"?>
<!DOCTYPE service_bundle SYSTEM "/usr/share/lib/xml/dtd/service_bundle.dtd.1">
<!--
Created by Manifold
--><service_bundle type="manifest" name="tautulli">
<service name="application/tautulli" type="service" version="1">
<create_default_instance enabled="true"/>
<single_instance/>
<dependency name="network" grouping="require_all" restart_on="error" type="service">
<service_fmri value="svc:/milestone/network:default"/>
</dependency>
<dependency name="filesystem" grouping="require_all" restart_on="error" type="service">
<service_fmri value="svc:/system/filesystem/local"/>
</dependency>
<method_context>
<method_credential user="tautulli" group="nogroup"/>
</method_context>
<exec_method type="method" name="start" exec="python /opt/Tautulli/Tautulli.py --daemon --quiet --nolaunch" timeout_seconds="60"/>
<exec_method type="method" name="stop" exec=":kill" timeout_seconds="60"/>
<property_group name="startd" type="framework">
<propval name="duration" type="astring" value="contract"/>
<propval name="ignore_error" type="astring" value="core,signal"/>
</property_group>
<stability value="Evolving"/>
<template>
<common_name>
<loctext xml:lang="C">
Tautulli
</loctext>
</common_name>
</template>
</service>
</service_bundle>

View File

@@ -28,14 +28,16 @@
# Ubuntu/Debian: sudo addgroup tautulli && sudo adduser --system --no-create-home tautulli --ingroup tautulli
# CentOS/Fedora: sudo adduser --system --no-create-home tautulli
# 2. Give the user ownership of the Tautulli directory:
# sudo chown tautulli:tautulli -R /opt/Tautulli
# sudo chown -R tautulli:tautulli /opt/Tautulli
#
# - Adjust ExecStart= to point to:
# 1. Your Tautulli executable
# 1. Your Python interpreter (get the path with "command -v python3")
# - Default: /usr/bin/python3
# 2. Your Tautulli executable
# - Default: /opt/Tautulli/Tautulli.py
# 2. Your config file (recommended is to put it somewhere in /etc)
# 3. Your config file (recommended is to put it somewhere in /etc)
# - Default: --config /opt/Tautulli/config.ini
# 3. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# 4. Your datadir (recommended is to NOT put it in your Tautulli exec dir)
# - Default: --datadir /opt/Tautulli
#
# - Adjust User= and Group= to the user/group you want Tautulli to run as.
@@ -50,7 +52,7 @@ Wants=network-online.target
After=network-online.target
[Service]
ExecStart=/opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
ExecStart=/usr/bin/python3 /opt/Tautulli/Tautulli.py --config /opt/Tautulli/config.ini --datadir /opt/Tautulli --quiet --daemon --nolaunch
GuessMainPID=no
Type=forking
User=tautulli

View File

@@ -1,209 +0,0 @@
#!/bin/sh
#
## Don't edit this file
## Edit user configuation in /etc/default/tautulli to change
##
## Make sure init script is executable
## sudo chmod +x /path/to/init.ubuntu
##
## Install the init script
## sudo ln -s /path/to/init.ubuntu /etc/init.d/tautulli
##
## Create the tautulli daemon user:
## sudo adduser --system --no-create-home tautulli
##
## Make sure /opt/Tautulli is owned by the tautulli user
## sudo chown tautulli:nogroup -R /opt/Tautulli
##
## Touch the default file to stop the warning message when starting
## sudo touch /etc/default/tautulli
##
## To start Tautulli automatically
## sudo update-rc.d tautulli defaults
##
## To start/stop/restart Tautulli
## sudo service tautulli start
## sudo service tautulli stop
## sudo service tautulli restart
##
## TAUTULLI_USER= #$RUN_AS, username to run Tautulli under, the default is tautulli
## TAUTULLI_HOME= #$APP_PATH, the location of Tautulli.py, the default is /opt/Tautulli
## TAUTULLI_DATA= #$DATA_DIR, the location of plexpy.db, cache, logs, the default is /opt/Tautulli
## TAUTULLI_PIDFILE= #$PID_FILE, the location of tautulli.pid, the default is /var/run/tautulli/tautulli.pid
## PYTHON_BIN= #$DAEMON, the location of the python binary, the default is /usr/bin/python
## TAUTULLI_OPTS= #$EXTRA_DAEMON_OPTS, extra cli option for Tautulli, i.e. " --config=/home/Tautulli/config.ini"
## SSD_OPTS= #$EXTRA_SSD_OPTS, extra start-stop-daemon option like " --group=users"
## TAUTULLI_PORT= #$PORT_OPTS, hardcoded port for the webserver, overrides value in config.ini
##
## EXAMPLE if want to run as different user
## add TAUTULLI_USER=username to /etc/default/tautulli
## otherwise default tautulli is used
#
### BEGIN INIT INFO
# Provides: tautulli
# Required-Start: $local_fs $network $remote_fs
# Required-Stop: $local_fs $network $remote_fs
# Should-Start: $NetworkManager
# Should-Stop: $NetworkManager
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: starts instance of Tautulli
# Description: starts instance of Tautulli using start-stop-daemon
### END INIT INFO
# Script name
NAME=tautulli
# App name
DESC=Tautulli
SETTINGS_LOADED=FALSE
. /lib/lsb/init-functions
# Source Tautulli configuration
if [ -f /etc/default/tautulli ]; then
SETTINGS=/etc/default/tautulli
else
log_warning_msg "/etc/default/tautulli not found using default settings.";
fi
check_retval() {
if [ $? -eq 0 ]; then
log_end_msg 0
return 0
else
log_end_msg 1
exit 1
fi
}
load_settings() {
if [ $SETTINGS_LOADED != "TRUE" ]; then
. $SETTINGS
## The defaults
# Run as username
RUN_AS=${TAUTULLI_USER-tautulli}
# Path to app TAUTULLI_HOME=path_to_app_Tautulli.py
APP_PATH=${TAUTULLI_HOME-/opt/Tautulli}
# Data directory where plexpy.db, cache and logs are stored
DATA_DIR=${TAUTULLI_DATA-/opt/Tautulli}
# Path to store PID file
PID_FILE=${TAUTULLI_PIDFILE-/var/run/tautulli/tautulli.pid}
# Path to python bin
DAEMON=${PYTHON_BIN-/usr/bin/python}
# Extra daemon option like: TAUTULLI_OPTS=" --config=/home/Tautulli/config.ini"
EXTRA_DAEMON_OPTS=${TAUTULLI_OPTS-}
# Extra start-stop-daemon option like START_OPTS=" --group=users"
EXTRA_SSD_OPTS=${SSD_OPTS-}
# Hardcoded port to run on, overrides config.ini settings
[ -n "$TAUTULLI_PORT" ] && {
PORT_OPTS=" --port=${TAUTULLI_PORT} "
}
DAEMON_OPTS=" Tautulli.py --quiet --daemon --nolaunch --pidfile=${PID_FILE} --datadir=${DATA_DIR} ${PORT_OPTS}${EXTRA_DAEMON_OPTS}"
SETTINGS_LOADED=TRUE
fi
[ -x $DAEMON ] || {
log_warning_msg "$DESC: Can't execute daemon, aborting. See $DAEMON";
return 1;}
return 0
}
load_settings || exit 0
is_running () {
# returns 1 when running, else 0.
if [ -e $PID_FILE ]; then
PID=`cat $PID_FILE`
RET=$?
[ $RET -gt 1 ] && exit 1 || return $RET
else
return 1
fi
}
handle_pid () {
PID_PATH=`dirname $PID_FILE`
[ -d $PID_PATH ] || mkdir -p $PID_PATH && chown -R $RUN_AS $PID_PATH > /dev/null || {
log_warning_msg "$DESC: Could not create $PID_FILE, See $SETTINGS, aborting.";
return 1;}
if [ -e $PID_FILE ]; then
PID=`cat $PID_FILE`
if ! kill -0 $PID > /dev/null 2>&1; then
log_warning_msg "Removing stale $PID_FILE"
rm $PID_FILE
fi
fi
}
handle_datadir () {
[ -d $DATA_DIR ] || mkdir -p $DATA_DIR && chown -R $RUN_AS $DATA_DIR > /dev/null || {
log_warning_msg "$DESC: Could not create $DATA_DIR, See $SETTINGS, aborting.";
return 1;}
}
handle_updates () {
chown -R $RUN_AS $APP_PATH > /dev/null || {
log_warning_msg "$DESC: $APP_PATH not writable by $RUN_AS for web-updates";
return 0; }
}
start_tautulli () {
handle_pid
handle_datadir
handle_updates
if ! is_running; then
log_daemon_msg "Starting $DESC"
start-stop-daemon -o -d $APP_PATH -c $RUN_AS --start $EXTRA_SSD_OPTS --pidfile $PID_FILE --exec $DAEMON -- $DAEMON_OPTS
check_retval
else
log_success_msg "$DESC: already running (pid $PID)"
fi
}
stop_tautulli () {
if is_running; then
log_daemon_msg "Stopping $DESC"
start-stop-daemon -o --stop --pidfile $PID_FILE --retry 15
check_retval
else
log_success_msg "$DESC: not running"
fi
}
case "$1" in
start)
start_tautulli
;;
stop)
stop_tautulli
;;
restart|force-reload)
stop_tautulli
start_tautulli
;;
status)
status_of_proc -p "$PID_FILE" "$DAEMON" "$DESC"
;;
*)
N=/etc/init.d/$NAME
echo "Usage: $N {start|stop|restart|force-reload|status}" >&2
exit 1
;;
esac
exit 0

View File

@@ -1,18 +0,0 @@
# tautulli
#
# This is a session/user job. Install this file into /usr/share/upstart/sessions
# if Tautulli is installed system wide, and into $XDG_CONFIG_HOME/upstart if
# Tautulli is installed per user. Change the executable path appropiately.
start on desktop-start
stop on desktop-end
env CONFIG=""$XDG_CONFIG_HOME"/Tautulli"
env DATA=""$XDG_DATA_HOME"/Tautulli"
pre-start script
[ -d "$CONFIG" ] || mkdir -p "$CONFIG"
[ -d "$DATA" ] || mkdir -p "$DATA"
end script
exec Tautulli.py --nolaunch --config "$CONFIG"/config.ini --datadir "$DATA"

View File

@@ -10,7 +10,7 @@
import sys, re, os
from cStringIO import StringIO
from io import StringIO
@@ -116,6 +116,6 @@ def consume(outbuffer = None): # Capture standard output
if __name__ == '__main__':
consume(sys.stdout).write(sys.stdin.read())
print '\n'
print('\n')
# vim: set nowrap tabstop=4 shiftwidth=4 softtabstop=0 expandtab textwidth=0 filetype=python foldmethod=indent foldcolumn=4

View File

@@ -1,16 +1,7 @@
###############################################################################
# Universal Analytics for Python
# Copyright (c) 2013, Analytics Pros
#
# This project is free software, distributed under the BSD license.
# Analytics Pros offers consulting and integration services if your firm needs
# assistance in strategy, implementation, or auditing existing work.
###############################################################################
from urllib2 import urlopen, build_opener, install_opener
from urllib2 import Request, HTTPSHandler
from urllib2 import URLError, HTTPError
from urllib import urlencode
from future.moves.urllib.request import urlopen, build_opener, install_opener
from future.moves.urllib.request import Request, HTTPSHandler
from future.moves.urllib.error import URLError, HTTPError
from future.moves.urllib.parse import urlencode
import random
import datetime
@@ -24,8 +15,8 @@ def generate_uuid(basedata=None):
""" Provides a _random_ UUID with no input, or a UUID4-format MD5 checksum of any input data provided """
if basedata is None:
return str(uuid.uuid4())
elif isinstance(basedata, basestring):
checksum = hashlib.md5(basedata).hexdigest()
elif isinstance(basedata, str):
checksum = hashlib.md5(str(basedata).encode('utf-8')).hexdigest()
return '%8s-%4s-%4s-%4s-%12s' % (
checksum[0:8], checksum[8:12], checksum[12:16], checksum[16:20], checksum[20:32])
@@ -44,7 +35,7 @@ class Time(datetime.datetime):
def to_unix(cls, timestamp):
""" Wrapper over time module to produce Unix epoch time as a float """
if not isinstance(timestamp, datetime.datetime):
raise TypeError, 'Time.milliseconds expects a datetime object'
raise TypeError('Time.milliseconds expects a datetime object')
base = time.mktime(timestamp.timetuple())
return base
@@ -86,14 +77,14 @@ class HTTPRequest(object):
def fixUTF8(cls, data): # Ensure proper encoding for UA's servers...
""" Convert all strings to UTF-8 """
for key in data:
if isinstance(data[key], basestring):
if isinstance(data[key], str):
data[key] = data[key].encode('utf-8')
return data
# Apply stored properties to the given dataset & POST to the configured endpoint
def send(self, data):
request = Request(
self.endpoint + '?' + urlencode(self.fixUTF8(data)),
self.endpoint + '?' + urlencode(self.fixUTF8(data)).encode('utf-8'),
headers={
'User-Agent': self.user_agent
}
@@ -121,7 +112,7 @@ class HTTPPost(HTTPRequest):
def send(self, data):
request = Request(
self.endpoint,
data=urlencode(self.fixUTF8(data)),
data=urlencode(self.fixUTF8(data)).encode('utf-8'),
headers={
'User-Agent': self.user_agent
}
@@ -144,26 +135,26 @@ class Tracker(object):
@classmethod
def coerceParameter(cls, name, value=None):
if isinstance(name, basestring) and name[0] == '&':
if isinstance(name, str) and name[0] == '&':
return name[1:], str(value)
elif name in cls.parameter_alias:
typecast, param_name = cls.parameter_alias.get(name)
return param_name, typecast(value)
else:
raise KeyError, 'Parameter "{0}" is not recognized'.format(name)
raise KeyError('Parameter "{0}" is not recognized'.format(name))
def payload(self, data):
for key, value in data.iteritems():
for key, value in data.items():
try:
yield self.coerceParameter(key, value)
except KeyError:
continue
option_sequence = {
'pageview': [(basestring, 'dp')],
'event': [(basestring, 'ec'), (basestring, 'ea'), (basestring, 'el'), (int, 'ev')],
'social': [(basestring, 'sn'), (basestring, 'sa'), (basestring, 'st')],
'timing': [(basestring, 'utc'), (basestring, 'utv'), (basestring, 'utt'), (basestring, 'utl')]
'pageview': [(str, 'dp')],
'event': [(str, 'ec'), (str, 'ea'), (str, 'el'), (int, 'ev')],
'social': [(str, 'sn'), (str, 'sa'), (str, 'st')],
'timing': [(str, 'utc'), (str, 'utv'), (str, 'utt'), (str, 'utl')]
}
@classmethod
@@ -232,7 +223,7 @@ class Tracker(object):
for key, val in self.payload(item):
data[key] = val
for k, v in self.params.iteritems(): # update only absent parameters
for k, v in self.params.items(): # update only absent parameters
if k not in data:
data[k] = v
@@ -247,13 +238,13 @@ class Tracker(object):
# Setting persistent attibutes of the session/hit/etc (inc. custom dimensions/metrics)
def set(self, name, value=None):
if isinstance(name, dict):
for key, value in name.iteritems():
for key, value in name.items():
try:
param, value = self.coerceParameter(key, value)
self.params[param] = value
except KeyError:
pass
elif isinstance(name, basestring):
elif isinstance(name, str):
try:
param, value = self.coerceParameter(name, value)
self.params[param] = value
@@ -277,7 +268,7 @@ class Tracker(object):
def safe_unicode(obj):
""" Safe convertion to the Unicode string version of the object """
try:
return unicode(obj)
return str(obj)
except UnicodeDecodeError:
return obj.decode('utf-8')
@@ -380,7 +371,7 @@ for i in range(0, 5):
# Enhanced Ecommerce
Tracker.alias(str, 'pa') # Product action
Tracker.alias(str, 'tcc') # Coupon code
Tracker.alias(unicode, 'pal') # Product action list
Tracker.alias(str, 'pal') # Product action list
Tracker.alias(int, 'cos') # Checkout step
Tracker.alias(str, 'col') # Checkout step option
@@ -388,10 +379,10 @@ Tracker.alias(str, 'promoa') # Promotion action
for product_index in range(1, MAX_EC_PRODUCTS):
Tracker.alias(str, 'pr{0}id'.format(product_index)) # Product SKU
Tracker.alias(unicode, 'pr{0}nm'.format(product_index)) # Product name
Tracker.alias(unicode, 'pr{0}br'.format(product_index)) # Product brand
Tracker.alias(unicode, 'pr{0}ca'.format(product_index)) # Product category
Tracker.alias(unicode, 'pr{0}va'.format(product_index)) # Product variant
Tracker.alias(str, 'pr{0}nm'.format(product_index)) # Product name
Tracker.alias(str, 'pr{0}br'.format(product_index)) # Product brand
Tracker.alias(str, 'pr{0}ca'.format(product_index)) # Product category
Tracker.alias(str, 'pr{0}va'.format(product_index)) # Product variant
Tracker.alias(str, 'pr{0}pr'.format(product_index)) # Product price
Tracker.alias(int, 'pr{0}qt'.format(product_index)) # Product quantity
Tracker.alias(str, 'pr{0}cc'.format(product_index)) # Product coupon code
@@ -403,10 +394,10 @@ for product_index in range(1, MAX_EC_PRODUCTS):
for list_index in range(1, MAX_EC_LISTS):
Tracker.alias(str, 'il{0}pi{1}id'.format(list_index, product_index)) # Product impression SKU
Tracker.alias(unicode, 'il{0}pi{1}nm'.format(list_index, product_index)) # Product impression name
Tracker.alias(unicode, 'il{0}pi{1}br'.format(list_index, product_index)) # Product impression brand
Tracker.alias(unicode, 'il{0}pi{1}ca'.format(list_index, product_index)) # Product impression category
Tracker.alias(unicode, 'il{0}pi{1}va'.format(list_index, product_index)) # Product impression variant
Tracker.alias(str, 'il{0}pi{1}nm'.format(list_index, product_index)) # Product impression name
Tracker.alias(str, 'il{0}pi{1}br'.format(list_index, product_index)) # Product impression brand
Tracker.alias(str, 'il{0}pi{1}ca'.format(list_index, product_index)) # Product impression category
Tracker.alias(str, 'il{0}pi{1}va'.format(list_index, product_index)) # Product impression variant
Tracker.alias(int, 'il{0}pi{1}ps'.format(list_index, product_index)) # Product impression position
Tracker.alias(int, 'il{0}pi{1}pr'.format(list_index, product_index)) # Product impression price
@@ -417,11 +408,11 @@ for product_index in range(1, MAX_EC_PRODUCTS):
custom_index)) # Product impression custom metric
for list_index in range(1, MAX_EC_LISTS):
Tracker.alias(unicode, 'il{0}nm'.format(list_index)) # Product impression list name
Tracker.alias(str, 'il{0}nm'.format(list_index)) # Product impression list name
for promotion_index in range(1, MAX_EC_PROMOTIONS):
Tracker.alias(str, 'promo{0}id'.format(promotion_index)) # Promotion ID
Tracker.alias(unicode, 'promo{0}nm'.format(promotion_index)) # Promotion name
Tracker.alias(str, 'promo{0}nm'.format(promotion_index)) # Promotion name
Tracker.alias(str, 'promo{0}cr'.format(promotion_index)) # Promotion creative
Tracker.alias(str, 'promo{0}ps'.format(promotion_index)) # Promotion position

View File

@@ -1 +1 @@
import Tracker
from . import Tracker

608
lib/appdirs.py Normal file
View File

@@ -0,0 +1,608 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright (c) 2005-2010 ActiveState Software Inc.
# Copyright (c) 2013 Eddy Petrișor
"""Utilities for determining application-specific dirs.
See <http://github.com/ActiveState/appdirs> for details and usage.
"""
# Dev Notes:
# - MSDN on where to store app data files:
# http://support.microsoft.com/default.aspx?scid=kb;en-us;310294#XSLTH3194121123120121120120
# - Mac OS X: http://developer.apple.com/documentation/MacOSX/Conceptual/BPFileSystem/index.html
# - XDG spec for Un*x: http://standards.freedesktop.org/basedir-spec/basedir-spec-latest.html
__version_info__ = (1, 4, 3)
__version__ = '.'.join(map(str, __version_info__))
import sys
import os
PY3 = sys.version_info[0] == 3
if PY3:
unicode = str
if sys.platform.startswith('java'):
import platform
os_name = platform.java_ver()[3][0]
if os_name.startswith('Windows'): # "Windows XP", "Windows 7", etc.
system = 'win32'
elif os_name.startswith('Mac'): # "Mac OS X", etc.
system = 'darwin'
else: # "Linux", "SunOS", "FreeBSD", etc.
# Setting this to "linux2" is not ideal, but only Windows or Mac
# are actually checked for and the rest of the module expects
# *sys.platform* style strings.
system = 'linux2'
else:
system = sys.platform
def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user data directories are:
Mac OS X: ~/Library/Application Support/<AppName>
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
Win 7 (not roaming): C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>
Win 7 (roaming): C:\Users\<username>\AppData\Roaming\<AppAuthor>\<AppName>
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
That means, by default "~/.local/share/<AppName>".
"""
if system == "win32":
if appauthor is None:
appauthor = appname
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
path = os.path.normpath(_get_win_folder(const))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
elif system == 'darwin':
path = os.path.expanduser('~/Library/Application Support/')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
r"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of data dirs should be
returned. By default, the first item from XDG_DATA_DIRS is
returned, or '/usr/local/share/<AppName>',
if XDG_DATA_DIRS is not set
Typical site data directories are:
Mac OS X: /Library/Application Support/<AppName>
Unix: /usr/local/share/<AppName> or /usr/share/<AppName>
Win XP: C:\Documents and Settings\All Users\Application Data\<AppAuthor>\<AppName>
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
Win 7: C:\ProgramData\<AppAuthor>\<AppName> # Hidden, but writeable on Win 7.
For Unix, this is using the $XDG_DATA_DIRS[0] default.
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
if system == "win32":
if appauthor is None:
appauthor = appname
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
elif system == 'darwin':
path = os.path.expanduser('/Library/Application Support')
if appname:
path = os.path.join(path, appname)
else:
# XDG default for $XDG_DATA_DIRS
# only first, if multipath is False
path = os.getenv('XDG_DATA_DIRS',
os.pathsep.join(['/usr/local/share', '/usr/share']))
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
if appname:
if version:
appname = os.path.join(appname, version)
pathlist = [os.sep.join([x, appname]) for x in pathlist]
if multipath:
path = os.pathsep.join(pathlist)
else:
path = pathlist[0]
return path
if appname and version:
path = os.path.join(path, version)
return path
def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific config dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user config directories are:
Mac OS X: same as user_data_dir
Unix: ~/.config/<AppName> # or in $XDG_CONFIG_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
That means, by default "~/.config/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
else:
path = os.getenv('XDG_CONFIG_HOME', os.path.expanduser("~/.config"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
r"""Return full path to the user-shared data dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"multipath" is an optional parameter only applicable to *nix
which indicates that the entire list of config dirs should be
returned. By default, the first item from XDG_CONFIG_DIRS is
returned, or '/etc/xdg/<AppName>', if XDG_CONFIG_DIRS is not set
Typical site config directories are:
Mac OS X: same as site_data_dir
Unix: /etc/xdg/<AppName> or $XDG_CONFIG_DIRS[i]/<AppName> for each value in
$XDG_CONFIG_DIRS
Win *: same as site_data_dir
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory on Vista.)
For Unix, this is using the $XDG_CONFIG_DIRS[0] default, if multipath=False
WARNING: Do not use this on Windows. See the Vista-Fail note above for why.
"""
if system in ["win32", "darwin"]:
path = site_data_dir(appname, appauthor)
if appname and version:
path = os.path.join(path, version)
else:
# XDG default for $XDG_CONFIG_DIRS
# only first, if multipath is False
path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)]
if appname:
if version:
appname = os.path.join(appname, version)
pathlist = [os.sep.join([x, appname]) for x in pathlist]
if multipath:
path = os.pathsep.join(pathlist)
else:
path = pathlist[0]
return path
def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific cache dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Cache" to the base app data dir for Windows. See
discussion below.
Typical user cache directories are:
Mac OS X: ~/Library/Caches/<AppName>
Unix: ~/.cache/<AppName> (XDG default)
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Cache
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Cache
On Windows the only suggestion in the MSDN docs is that local settings go in
the `CSIDL_LOCAL_APPDATA` directory. This is identical to the non-roaming
app data dir (the default returned by `user_data_dir` above). Apps typically
put cache data somewhere *under* the given dir here. Some examples:
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
...\Acme\SuperApp\Cache\1.0
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
This can be disabled with the `opinion=False` option.
"""
if system == "win32":
if appauthor is None:
appauthor = appname
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
if appname:
if appauthor is not False:
path = os.path.join(path, appauthor, appname)
else:
path = os.path.join(path, appname)
if opinion:
path = os.path.join(path, "Cache")
elif system == 'darwin':
path = os.path.expanduser('~/Library/Caches')
if appname:
path = os.path.join(path, appname)
else:
path = os.getenv('XDG_CACHE_HOME', os.path.expanduser('~/.cache'))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def user_state_dir(appname=None, appauthor=None, version=None, roaming=False):
r"""Return full path to the user-specific state dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"roaming" (boolean, default False) can be set True to use the Windows
roaming appdata directory. That means that for users on a Windows
network setup for roaming profiles, this user data will be
sync'd on login. See
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
for a discussion of issues.
Typical user state directories are:
Mac OS X: same as user_data_dir
Unix: ~/.local/state/<AppName> # or in $XDG_STATE_HOME, if defined
Win *: same as user_data_dir
For Unix, we follow this Debian proposal <https://wiki.debian.org/XDGBaseDirectorySpecification#state>
to extend the XDG spec and support $XDG_STATE_HOME.
That means, by default "~/.local/state/<AppName>".
"""
if system in ["win32", "darwin"]:
path = user_data_dir(appname, appauthor, None, roaming)
else:
path = os.getenv('XDG_STATE_HOME', os.path.expanduser("~/.local/state"))
if appname:
path = os.path.join(path, appname)
if appname and version:
path = os.path.join(path, version)
return path
def user_log_dir(appname=None, appauthor=None, version=None, opinion=True):
r"""Return full path to the user-specific log dir for this application.
"appname" is the name of application.
If None, just the system directory is returned.
"appauthor" (only used on Windows) is the name of the
appauthor or distributing body for this application. Typically
it is the owning company name. This falls back to appname. You may
pass False to disable it.
"version" is an optional version path element to append to the
path. You might want to use this if you want multiple versions
of your app to be able to run independently. If used, this
would typically be "<major>.<minor>".
Only applied when appname is present.
"opinion" (boolean) can be False to disable the appending of
"Logs" to the base app data dir for Windows, and "log" to the
base cache dir for Unix. See discussion below.
Typical user log directories are:
Mac OS X: ~/Library/Logs/<AppName>
Unix: ~/.cache/<AppName>/log # or under $XDG_CACHE_HOME if defined
Win XP: C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>\Logs
Vista: C:\Users\<username>\AppData\Local\<AppAuthor>\<AppName>\Logs
On Windows the only suggestion in the MSDN docs is that local settings
go in the `CSIDL_LOCAL_APPDATA` directory. (Note: I'm interested in
examples of what some windows apps use for a logs dir.)
OPINION: This function appends "Logs" to the `CSIDL_LOCAL_APPDATA`
value for Windows and appends "log" to the user cache dir for Unix.
This can be disabled with the `opinion=False` option.
"""
if system == "darwin":
path = os.path.join(
os.path.expanduser('~/Library/Logs'),
appname)
elif system == "win32":
path = user_data_dir(appname, appauthor, version)
version = False
if opinion:
path = os.path.join(path, "Logs")
else:
path = user_cache_dir(appname, appauthor, version)
version = False
if opinion:
path = os.path.join(path, "log")
if appname and version:
path = os.path.join(path, version)
return path
class AppDirs(object):
"""Convenience wrapper for getting application dirs."""
def __init__(self, appname=None, appauthor=None, version=None,
roaming=False, multipath=False):
self.appname = appname
self.appauthor = appauthor
self.version = version
self.roaming = roaming
self.multipath = multipath
@property
def user_data_dir(self):
return user_data_dir(self.appname, self.appauthor,
version=self.version, roaming=self.roaming)
@property
def site_data_dir(self):
return site_data_dir(self.appname, self.appauthor,
version=self.version, multipath=self.multipath)
@property
def user_config_dir(self):
return user_config_dir(self.appname, self.appauthor,
version=self.version, roaming=self.roaming)
@property
def site_config_dir(self):
return site_config_dir(self.appname, self.appauthor,
version=self.version, multipath=self.multipath)
@property
def user_cache_dir(self):
return user_cache_dir(self.appname, self.appauthor,
version=self.version)
@property
def user_state_dir(self):
return user_state_dir(self.appname, self.appauthor,
version=self.version)
@property
def user_log_dir(self):
return user_log_dir(self.appname, self.appauthor,
version=self.version)
#---- internal support stuff
def _get_win_folder_from_registry(csidl_name):
"""This is a fallback technique at best. I'm not sure if using the
registry for this guarantees us the correct answer for all CSIDL_*
names.
"""
if PY3:
import winreg as _winreg
else:
import _winreg
shell_folder_name = {
"CSIDL_APPDATA": "AppData",
"CSIDL_COMMON_APPDATA": "Common AppData",
"CSIDL_LOCAL_APPDATA": "Local AppData",
}[csidl_name]
key = _winreg.OpenKey(
_winreg.HKEY_CURRENT_USER,
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
)
dir, type = _winreg.QueryValueEx(key, shell_folder_name)
return dir
def _get_win_folder_with_pywin32(csidl_name):
from win32com.shell import shellcon, shell
dir = shell.SHGetFolderPath(0, getattr(shellcon, csidl_name), 0, 0)
# Try to make this a unicode path because SHGetFolderPath does
# not return unicode strings when there is unicode data in the
# path.
try:
dir = unicode(dir)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in dir:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
try:
import win32api
dir = win32api.GetShortPathName(dir)
except ImportError:
pass
except UnicodeError:
pass
return dir
def _get_win_folder_with_ctypes(csidl_name):
import ctypes
csidl_const = {
"CSIDL_APPDATA": 26,
"CSIDL_COMMON_APPDATA": 35,
"CSIDL_LOCAL_APPDATA": 28,
}[csidl_name]
buf = ctypes.create_unicode_buffer(1024)
ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in buf:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf2 = ctypes.create_unicode_buffer(1024)
if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
buf = buf2
return buf.value
def _get_win_folder_with_jna(csidl_name):
import array
from com.sun import jna
from com.sun.jna.platform import win32
buf_size = win32.WinDef.MAX_PATH * 2
buf = array.zeros('c', buf_size)
shell = win32.Shell32.INSTANCE
shell.SHGetFolderPath(None, getattr(win32.ShlObj, csidl_name), None, win32.ShlObj.SHGFP_TYPE_CURRENT, buf)
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
# Downgrade to short path name if have highbit chars. See
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
has_high_char = False
for c in dir:
if ord(c) > 255:
has_high_char = True
break
if has_high_char:
buf = array.zeros('c', buf_size)
kernel = win32.Kernel32.INSTANCE
if kernel.GetShortPathName(dir, buf, buf_size):
dir = jna.Native.toString(buf.tostring()).rstrip("\0")
return dir
if system == "win32":
try:
import win32com.shell
_get_win_folder = _get_win_folder_with_pywin32
except ImportError:
try:
from ctypes import windll
_get_win_folder = _get_win_folder_with_ctypes
except ImportError:
try:
import com.sun.jna
_get_win_folder = _get_win_folder_with_jna
except ImportError:
_get_win_folder = _get_win_folder_from_registry
#---- self test code
if __name__ == "__main__":
appname = "MyApp"
appauthor = "MyCompany"
props = ("user_data_dir",
"user_config_dir",
"user_cache_dir",
"user_state_dir",
"user_log_dir",
"site_data_dir",
"site_config_dir")
print("-- app dirs %s --" % __version__)
print("-- app dirs (with optional 'version')")
dirs = AppDirs(appname, appauthor, version="1.0")
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (without optional 'version')")
dirs = AppDirs(appname, appauthor)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (without optional 'appauthor')")
dirs = AppDirs(appname)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))
print("\n-- app dirs (with disabled 'appauthor')")
dirs = AppDirs(appname, appauthor=False)
for prop in props:
print("%s: %s" % (prop, getattr(dirs, prop)))

View File

@@ -3,7 +3,7 @@ __all__ = ('EVENT_SCHEDULER_STARTED', 'EVENT_SCHEDULER_SHUTDOWN', 'EVENT_SCHEDUL
'EVENT_JOBSTORE_ADDED', 'EVENT_JOBSTORE_REMOVED', 'EVENT_ALL_JOBS_REMOVED',
'EVENT_JOB_ADDED', 'EVENT_JOB_REMOVED', 'EVENT_JOB_MODIFIED', 'EVENT_JOB_EXECUTED',
'EVENT_JOB_ERROR', 'EVENT_JOB_MISSED', 'EVENT_JOB_SUBMITTED', 'EVENT_JOB_MAX_INSTANCES',
'SchedulerEvent', 'JobEvent', 'JobExecutionEvent')
'SchedulerEvent', 'JobEvent', 'JobExecutionEvent', 'JobSubmissionEvent')
EVENT_SCHEDULER_STARTED = EVENT_SCHEDULER_START = 2 ** 0

View File

@@ -3,12 +3,11 @@ from __future__ import absolute_import
import sys
from apscheduler.executors.base import BaseExecutor, run_job
from apscheduler.util import iscoroutinefunction_partial
try:
from asyncio import iscoroutinefunction
from apscheduler.executors.base_py3 import run_coroutine_job
except ImportError:
from trollius import iscoroutinefunction
run_coroutine_job = None
@@ -46,7 +45,7 @@ class AsyncIOExecutor(BaseExecutor):
else:
self._run_job_success(job.id, events)
if iscoroutinefunction(job.func):
if iscoroutinefunction_partial(job.func):
if run_coroutine_job is not None:
coro = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
f = self._eventloop.create_task(coro)

View File

@@ -8,10 +8,10 @@ from tornado.gen import convert_yielded
from apscheduler.executors.base import BaseExecutor, run_job
try:
from inspect import iscoroutinefunction
from apscheduler.executors.base_py3 import run_coroutine_job
from apscheduler.util import iscoroutinefunction_partial
except ImportError:
def iscoroutinefunction(func):
def iscoroutinefunction_partial(func):
return False
@@ -44,7 +44,7 @@ class TornadoExecutor(BaseExecutor):
else:
self._run_job_success(job.id, events)
if iscoroutinefunction(job.func):
if iscoroutinefunction_partial(job.func):
f = run_coroutine_job(job, job._jobstore_alias, run_times, self._logger.name)
else:
f = self.executor.submit(run_job, job, job._jobstore_alias, run_times,

View File

@@ -1,4 +1,4 @@
from collections import Iterable, Mapping
from inspect import ismethod, isclass
from uuid import uuid4
import six
@@ -8,6 +8,11 @@ from apscheduler.util import (
ref_to_obj, obj_to_ref, datetime_repr, repr_escape, get_callable_name, check_callable_args,
convert_to_datetime)
try:
from collections.abc import Iterable, Mapping
except ImportError:
from collections import Iterable, Mapping
class Job(object):
"""
@@ -235,13 +240,20 @@ class Job(object):
'be determined. Consider giving a textual reference (module:function name) '
'instead.' % (self.func,))
# Instance methods cannot survive serialization as-is, so store the "self" argument
# explicitly
if ismethod(self.func) and not isclass(self.func.__self__):
args = (self.func.__self__,) + tuple(self.args)
else:
args = self.args
return {
'version': 1,
'id': self.id,
'func': self.func_ref,
'trigger': self.trigger,
'executor': self.executor,
'args': self.args,
'args': args,
'kwargs': self.kwargs,
'name': self.name,
'misfire_grace_time': self.misfire_grace_time,

View File

@@ -14,7 +14,7 @@ except ImportError: # pragma: nocover
import pickle
try:
from redis import StrictRedis
from redis import Redis
except ImportError: # pragma: nocover
raise ImportError('RedisJobStore requires redis installed')
@@ -47,7 +47,7 @@ class RedisJobStore(BaseJobStore):
self.pickle_protocol = pickle_protocol
self.jobs_key = jobs_key
self.run_times_key = run_times_key
self.redis = StrictRedis(db=int(db), **connect_args)
self.redis = Redis(db=int(db), **connect_args)
def lookup_job(self, job_id):
job_state = self.redis.hget(self.jobs_key, job_id)
@@ -81,7 +81,9 @@ class RedisJobStore(BaseJobStore):
pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
self.pickle_protocol))
if job.next_run_time:
pipe.zadd(self.run_times_key, datetime_to_utc_timestamp(job.next_run_time), job.id)
pipe.zadd(self.run_times_key,
{job.id: datetime_to_utc_timestamp(job.next_run_time)})
pipe.execute()
def update_job(self, job):
@@ -92,9 +94,11 @@ class RedisJobStore(BaseJobStore):
pipe.hset(self.jobs_key, job.id, pickle.dumps(job.__getstate__(),
self.pickle_protocol))
if job.next_run_time:
pipe.zadd(self.run_times_key, datetime_to_utc_timestamp(job.next_run_time), job.id)
pipe.zadd(self.run_times_key,
{job.id: datetime_to_utc_timestamp(job.next_run_time)})
else:
pipe.zrem(self.run_times_key, job.id)
pipe.execute()
def remove_job(self, job_id):

View File

@@ -10,7 +10,7 @@ except ImportError: # pragma: nocover
import pickle
try:
import rethinkdb as r
from rethinkdb import RethinkDB
except ImportError: # pragma: nocover
raise ImportError('RethinkDBJobStore requires rethinkdb installed')
@@ -40,10 +40,12 @@ class RethinkDBJobStore(BaseJobStore):
raise ValueError('The "table" parameter must not be empty')
self.database = database
self.table = table
self.table_name = table
self.table = None
self.client = client
self.pickle_protocol = pickle_protocol
self.connect_args = connect_args
self.r = RethinkDB()
self.conn = None
def start(self, scheduler, alias):
@@ -52,31 +54,31 @@ class RethinkDBJobStore(BaseJobStore):
if self.client:
self.conn = maybe_ref(self.client)
else:
self.conn = r.connect(db=self.database, **self.connect_args)
self.conn = self.r.connect(db=self.database, **self.connect_args)
if self.database not in r.db_list().run(self.conn):
r.db_create(self.database).run(self.conn)
if self.database not in self.r.db_list().run(self.conn):
self.r.db_create(self.database).run(self.conn)
if self.table not in r.table_list().run(self.conn):
r.table_create(self.table).run(self.conn)
if self.table_name not in self.r.table_list().run(self.conn):
self.r.table_create(self.table_name).run(self.conn)
if 'next_run_time' not in r.table(self.table).index_list().run(self.conn):
r.table(self.table).index_create('next_run_time').run(self.conn)
if 'next_run_time' not in self.r.table(self.table_name).index_list().run(self.conn):
self.r.table(self.table_name).index_create('next_run_time').run(self.conn)
self.table = r.db(self.database).table(self.table)
self.table = self.r.db(self.database).table(self.table_name)
def lookup_job(self, job_id):
results = list(self.table.get_all(job_id).pluck('job_state').run(self.conn))
return self._reconstitute_job(results[0]['job_state']) if results else None
def get_due_jobs(self, now):
return self._get_jobs(r.row['next_run_time'] <= datetime_to_utc_timestamp(now))
return self._get_jobs(self.r.row['next_run_time'] <= datetime_to_utc_timestamp(now))
def get_next_run_time(self):
results = list(
self.table
.filter(r.row['next_run_time'] != None) # flake8: noqa
.order_by(r.asc('next_run_time'))
.filter(self.r.row['next_run_time'] != None) # noqa
.order_by(self.r.asc('next_run_time'))
.map(lambda x: x['next_run_time'])
.limit(1)
.run(self.conn)
@@ -92,7 +94,7 @@ class RethinkDBJobStore(BaseJobStore):
job_dict = {
'id': job.id,
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
'job_state': r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
}
results = self.table.insert(job_dict).run(self.conn)
if results['errors'] > 0:
@@ -101,7 +103,7 @@ class RethinkDBJobStore(BaseJobStore):
def update_job(self, job):
changes = {
'next_run_time': datetime_to_utc_timestamp(job.next_run_time),
'job_state': r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
'job_state': self.r.binary(pickle.dumps(job.__getstate__(), self.pickle_protocol))
}
results = self.table.get_all(job.id).update(changes).run(self.conn)
skipped = False in map(lambda x: results[x] == 0, results.keys())
@@ -130,20 +132,20 @@ class RethinkDBJobStore(BaseJobStore):
def _get_jobs(self, predicate=None):
jobs = []
failed_job_ids = []
query = (self.table.filter(r.row['next_run_time'] != None).filter(predicate) if
predicate else self.table)
query = (self.table.filter(self.r.row['next_run_time'] != None).filter(predicate) # noqa
if predicate else self.table)
query = query.order_by('next_run_time', 'id').pluck('id', 'job_state')
for document in query.run(self.conn):
try:
jobs.append(self._reconstitute_job(document['job_state']))
except:
except Exception:
self._logger.exception('Unable to restore job "%s" -- removing it', document['id'])
failed_job_ids.append(document['id'])
# Remove all the jobs we failed to restore
if failed_job_ids:
r.expr(failed_job_ids).for_each(
self.r.expr(failed_job_ids).for_each(
lambda job_id: self.table.get_all(job_id).delete()).run(self.conn)
return jobs

View File

@@ -106,7 +106,7 @@ class SQLAlchemyJobStore(BaseJobStore):
}).where(self.jobs_t.c.id == job.id)
result = self.engine.execute(update)
if result.rowcount == 0:
raise JobLookupError(id)
raise JobLookupError(job.id)
def remove_job(self, job_id):
delete = self.jobs_t.delete().where(self.jobs_t.c.id == job_id)

View File

@@ -1,7 +1,6 @@
from __future__ import print_function
from abc import ABCMeta, abstractmethod
from collections import MutableMapping
from threading import RLock
from datetime import datetime, timedelta
from logging import getLogger
@@ -19,13 +18,19 @@ from apscheduler.jobstores.base import ConflictingIdError, JobLookupError, BaseJ
from apscheduler.jobstores.memory import MemoryJobStore
from apscheduler.job import Job
from apscheduler.triggers.base import BaseTrigger
from apscheduler.util import asbool, asint, astimezone, maybe_ref, timedelta_seconds, undefined
from apscheduler.util import (
asbool, asint, astimezone, maybe_ref, timedelta_seconds, undefined, TIMEOUT_MAX)
from apscheduler.events import (
SchedulerEvent, JobEvent, JobSubmissionEvent, EVENT_SCHEDULER_START, EVENT_SCHEDULER_SHUTDOWN,
EVENT_JOBSTORE_ADDED, EVENT_JOBSTORE_REMOVED, EVENT_ALL, EVENT_JOB_MODIFIED, EVENT_JOB_REMOVED,
EVENT_JOB_ADDED, EVENT_EXECUTOR_ADDED, EVENT_EXECUTOR_REMOVED, EVENT_ALL_JOBS_REMOVED,
EVENT_JOB_SUBMITTED, EVENT_JOB_MAX_INSTANCES, EVENT_SCHEDULER_RESUMED, EVENT_SCHEDULER_PAUSED)
try:
from collections.abc import MutableMapping
except ImportError:
from collections import MutableMapping
#: constant indicating a scheduler's stopped state
STATE_STOPPED = 0
#: constant indicating a scheduler's running state (started and processing jobs)
@@ -126,11 +131,14 @@ class BaseScheduler(six.with_metaclass(ABCMeta)):
:param bool paused: if ``True``, don't start job processing until :meth:`resume` is called
:raises SchedulerAlreadyRunningError: if the scheduler is already running
:raises RuntimeError: if running under uWSGI with threads disabled
"""
if self.state != STATE_STOPPED:
raise SchedulerAlreadyRunningError
self._check_uwsgi()
with self._executors_lock:
# Create a default executor if nothing else is configured
if 'default' not in self._executors:
@@ -177,12 +185,13 @@ class BaseScheduler(six.with_metaclass(ABCMeta)):
self.state = STATE_STOPPED
with self._jobstores_lock, self._executors_lock:
# Shut down all executors
# Shut down all executors
with self._executors_lock:
for executor in six.itervalues(self._executors):
executor.shutdown(wait)
# Shut down all job stores
# Shut down all job stores
with self._jobstores_lock:
for jobstore in six.itervalues(self._jobstores):
jobstore.shutdown()
@@ -546,7 +555,7 @@ class BaseScheduler(six.with_metaclass(ABCMeta)):
"""
if pending is not None:
warnings.warn('The "pending" option is deprecated -- get_jobs() always returns '
'pending jobs if the scheduler has been started and scheduled jobs '
'scheduled jobs if the scheduler has been started and pending jobs '
'otherwise', DeprecationWarning)
with self._jobstores_lock:
@@ -589,14 +598,13 @@ class BaseScheduler(six.with_metaclass(ABCMeta)):
"""
jobstore_alias = None
with self._jobstores_lock:
# Check if the job is among the pending jobs
if self.state == STATE_STOPPED:
# Check if the job is among the pending jobs
if self.state == STATE_STOPPED:
for i, (job, alias, replace_existing) in enumerate(self._pending_jobs):
if job.id == job_id and jobstore in (None, alias):
del self._pending_jobs[i]
jobstore_alias = alias
break
for i, (job, alias, replace_existing) in enumerate(self._pending_jobs):
if job.id == job_id and jobstore in (None, alias):
del self._pending_jobs[i]
jobstore_alias = alias
break
else:
# Otherwise, try to remove it from each store until it succeeds or we run out of
# stores to check
@@ -824,6 +832,14 @@ class BaseScheduler(six.with_metaclass(ABCMeta)):
except BaseException:
self._logger.exception('Error notifying listener')
def _check_uwsgi(self):
"""Check if we're running under uWSGI with threads disabled."""
uwsgi_module = sys.modules.get('uwsgi')
if not getattr(uwsgi_module, 'has_threads', True):
raise RuntimeError('The scheduler seems to be running under uWSGI, but threads have '
'been disabled. You must run uWSGI with the --enable-threads '
'option for the scheduler to work.')
def _real_add_job(self, job, jobstore_alias, replace_existing):
"""
:param Job job: the job to add
@@ -999,7 +1015,7 @@ class BaseScheduler(six.with_metaclass(ABCMeta)):
wait_seconds = None
self._logger.debug('No jobs; waiting until a job is added')
else:
wait_seconds = max(timedelta_seconds(next_wakeup_time - now), 0)
wait_seconds = min(max(timedelta_seconds(next_wakeup_time - now), 0), TIMEOUT_MAX)
self._logger.debug('Next wakeup is due at %s (in %f seconds)', next_wakeup_time,
wait_seconds)

View File

@@ -9,7 +9,7 @@ except (ImportError, RuntimeError): # pragma: nocover
from PyQt4.QtCore import QObject, QTimer
except ImportError:
try:
from PySide.QtCore import QObject, QTimer # flake8: noqa
from PySide.QtCore import QObject, QTimer # noqa
except ImportError:
raise ImportError('QtScheduler requires either PyQt5, PyQt4 or PySide installed')
@@ -26,7 +26,8 @@ class QtScheduler(BaseScheduler):
def _start_timer(self, wait_seconds):
self._stop_timer()
if wait_seconds is not None:
self._timer = QTimer.singleShot(wait_seconds * 1000, self._process_jobs)
wait_time = min(wait_seconds * 1000, 2147483647)
self._timer = QTimer.singleShot(wait_time, self._process_jobs)
def _stop_timer(self):
if self._timer:

View File

@@ -192,9 +192,8 @@ class CronTrigger(BaseTrigger):
return None
if fieldnum >= 0:
if self.jitter is not None:
next_date = self._apply_jitter(next_date, self.jitter, now)
return next_date
next_date = self._apply_jitter(next_date, self.jitter, now)
return min(next_date, self.end_date) if self.end_date else next_date
def __getstate__(self):
return {

View File

@@ -9,7 +9,7 @@ __all__ = ('AllExpression', 'RangeExpression', 'WeekdayRangeExpression',
'WeekdayPositionExpression', 'LastDayOfMonthExpression')
WEEKDAYS = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat']
WEEKDAYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']
MONTHS = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', 'aug', 'sep', 'oct', 'nov', 'dec']

View File

@@ -104,7 +104,7 @@ class DayOfWeekField(BaseField):
COMPILERS = BaseField.COMPILERS + [WeekdayRangeExpression]
def get_value(self, dateval):
return dateval.isoweekday() % 7
return dateval.weekday()
class MonthField(BaseField):

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